From 7413ce08d4fa66eac8997ce43289c9931c635a9d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 17 May 2026 12:54:21 -0500 Subject: [PATCH 01/94] Merge detector and model in settings UI (#23216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add embedded mode to BaseSection so parents can host the save action * add optional action slot to current Frigate+ model summary * add w-full to action slot flex wrapper for explicit width contract * i18n * merged detectors and model settings view * fix document title * Embed detector form in merged settings view * add detection model card with tabs and custom model embed * add Frigate+ model selector with filter popover to merged page * Add mismatch banner and gate save on detector and model compatibility * Wire atomic save, restart toast, and undo on detectors and model page * Clear child pending data on undo * route merged detectors and model view in settings * trim Frigate+ page to account-only and remove old detection model view * basic e2e * Fix unsaved-changes guard, custom path leak, and post-failure cache resync * Rename to Detectors and model, float Modified badge, use ConfigMessageBanner for mismatch * Hide Plus/Custom tabs when Frigate+ is not enabled * Detect active Plus model via model.plus.id instead of path prefix * Sync state back to snapshot when child form un-modifies and remount on undo * Always require restart on save since model changes also need one * Wrap Frigate+ model selector in SplitCardRow with label and description * rename tab * update docs * sync top-level model with default detector's resolved model when the user doesn't define a top-level `model:` block, `FrigateConfig.model` stayed at pydantic field defaults (320×320, /labelmap.txt) while the per-detector model picked up `DEFAULT_MODEL` for openvino on cpu (300×300, coco_91cl_bkgr.txt introduced in #23127), causing `RemoteObjectDetector` to fail with "buffer is too small for requested array" because the SHM was sized from the per-detector model but mapped using the top-level one. After the detector loop, copy the first detector's resolved model up to `self.model` so both sides agree on dimensions and labelmap * revert to cpu detector by default use openvino cpu for new configs only * add defaults --- docs/docs/configuration/advanced.md | 2 +- docs/docs/configuration/index.md | 8 +- docs/docs/configuration/object_detectors.md | 86 +- docs/docs/guides/getting_started.md | 6 +- docs/docs/integrations/plus.md | 4 +- docs/scripts/lib/nav_map.py | 4 +- frigate/config/config.py | 15 +- frigate/test/test_config.py | 6 +- .../settings/detectors-and-model.spec.ts | 55 ++ web/public/locales/en/views/settings.json | 39 +- .../config-form/ConfigMessageBanner.tsx | 2 +- .../config-form/section-configs/types.ts | 2 + .../config-form/sections/BaseSection.tsx | 214 ++--- web/src/pages/Settings.tsx | 22 +- .../DetectorsAndModelSettingsView.tsx | 797 ++++++++++++++++++ .../settings/FrigatePlusSettingsView.tsx | 462 +--------- .../SystemDetectionModelSettingsView.tsx | 88 -- .../FrigatePlusCurrentModelSummary.tsx | 14 +- 18 files changed, 1107 insertions(+), 719 deletions(-) create mode 100644 web/e2e/specs/settings/detectors-and-model.spec.ts create mode 100644 web/src/views/settings/DetectorsAndModelSettingsView.tsx delete mode 100644 web/src/views/settings/SystemDetectionModelSettingsView.tsx diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md index e6de72593b..6ae023edac 100644 --- a/docs/docs/configuration/advanced.md +++ b/docs/docs/configuration/advanced.md @@ -172,7 +172,7 @@ Custom models may also require different input tensor formats. The colorspace co -Navigate to to configure the model path, dimensions, and input format. +Navigate to and open the **Custom Model** tab to configure the model path, dimensions, and input format. | Field | Description | | --------------------------------------------- | ------------------------------------ | diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 84f9780784..72f0cfd078 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -110,7 +110,7 @@ Here are some common starter configuration examples. These can be configured thr 1. Navigate to and configure the MQTT connection to your Home Assistant Mosquitto broker 2. Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` -3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` +3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` 4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` 6. Navigate to and add your camera with the appropriate RTSP stream URL @@ -189,7 +189,7 @@ cameras: 1. Navigate to and set **Enable MQTT** to off 2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)` -3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` +3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` 4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` 6. Navigate to and add your camera with the appropriate RTSP stream URL @@ -266,8 +266,8 @@ cameras: 1. Navigate to and configure the connection to your MQTT broker 2. Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)` -3. Navigate to and add a detector with **Type** `openvino` and **Device** `AUTO` -4. Navigate to and configure the OpenVINO model path and settings +3. Navigate to and add a detector with **Type** `openvino` and **Device** `AUTO` +4. On the same page, in the **Custom Model** tab, configure the OpenVINO model path and settings 5. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 6. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` 7. Navigate to and add your camera with the appropriate RTSP stream URL diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index 767f70ab9e..b1baf690c0 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -91,7 +91,7 @@ See [common Edge TPU troubleshooting steps](/troubleshooting/edgetpu) if the Edg -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. @@ -111,7 +111,7 @@ detectors: -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each. +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `usb:0` and `usb:1` as the device for each. @@ -136,7 +136,7 @@ _warning: may have [compatibility issues](https://github.com/blakeblackshear/fri -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty. +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then leave the device field empty. @@ -156,7 +156,7 @@ detectors: -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`. +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `pci`. @@ -176,7 +176,7 @@ detectors: -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each. +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors, specifying `pci:0` and `pci:1` as the device for each. @@ -199,7 +199,7 @@ detectors: -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`). +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add** to add multiple detectors with different device types (e.g., `usb` and `pci`). @@ -246,7 +246,7 @@ After placing the downloaded files for the tflite model and labels in your confi -Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then navigate to and configure the model settings: +Navigate to and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings: | Field | Value | | ---------------------------------------- | ----------------------------------------------------------------- | @@ -309,7 +309,7 @@ Use this configuration for YOLO-based models. When no custom model path or URL i -Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings: +Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings: | Field | Value | | ---------------------------------------- | ----------------------- | @@ -365,7 +365,7 @@ For SSD-based models, provide either a model path or URL to your compiled SSD mo -Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings: +Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings: | Field | Value | | --------------------------------------- | ------ | @@ -410,7 +410,7 @@ The Hailo detector supports all YOLO models compiled for Hailo hardware that inc -Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then navigate to and configure the model settings to match your custom model dimensions and format. +Navigate to and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings to match your custom model dimensions and format. @@ -465,7 +465,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`. +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`. @@ -508,7 +508,7 @@ Use the model configuration shown below when using the OpenVINO detector with th -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to and configure: +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------ | @@ -558,7 +558,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure: +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------- | @@ -620,7 +620,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then navigate to and configure: +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -676,7 +676,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then navigate to and configure: +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | --------------------------------------- | --------------------------------- | @@ -728,7 +728,7 @@ After placing the downloaded onnx model in your config/model_cache folder, use t -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then navigate to and configure: +Navigate to and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ---------------------------------- | @@ -807,7 +807,7 @@ Using the detector config below will connect to the client: -Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. +Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. @@ -841,7 +841,7 @@ When Frigate is started with the following config it will connect to the detecto -Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then navigate to and configure: +Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1002,7 +1002,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors. +Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors. @@ -1050,7 +1050,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------- | @@ -1109,7 +1109,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1158,7 +1158,7 @@ After placing the downloaded onnx model in your config folder, use the following -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------------- | @@ -1207,7 +1207,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | --------------------------------------- | --------------------------------- | @@ -1252,7 +1252,7 @@ After placing the downloaded onnx model in your `config/model_cache` folder, use -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------- | @@ -1328,7 +1328,7 @@ A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and -Navigate to and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended). +Navigate to and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended). @@ -1364,7 +1364,7 @@ To integrate CodeProject.AI into Frigate, configure the detector as follows: -Navigate to and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://:/v1/vision/detection`). +Navigate to and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://:/v1/vision/detection`). @@ -1403,7 +1403,7 @@ To configure the MemryX detector, use the following example configuration: -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. @@ -1423,7 +1423,7 @@ detectors: -Navigate to and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each. +Navigate to and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each. @@ -1467,7 +1467,7 @@ Below is the recommended configuration for using the **YOLO-NAS** (small) model -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------- | @@ -1515,7 +1515,7 @@ Below is the recommended configuration for using the **YOLOv9** (small) model wi -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------- | @@ -1562,7 +1562,7 @@ Below is the recommended configuration for using the **YOLOX** (small) model wit -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ----------------------- | @@ -1609,7 +1609,7 @@ Below is the recommended configuration for using the **SSDLite MobileNet v2** mo -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then navigate to and configure: +Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ----------------------- | @@ -1768,7 +1768,7 @@ Use the config below to work with generated TRT models: -Navigate to and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then navigate to and configure: +Navigate to and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ------------------------------------------------------------ | @@ -1825,7 +1825,7 @@ Use the model configuration shown below when using the synaptics detector with t -Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ---------------------------- | @@ -1879,7 +1879,7 @@ When using many cameras one detector may not be enough to keep up. Multiple dete -Navigate to and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection. +Navigate to and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection. @@ -1921,7 +1921,7 @@ This `config.yml` shows all relevant options to configure the detector and expla -Navigate to and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588). +Navigate to and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588). @@ -1958,7 +1958,7 @@ The inference time was determined on a rk3588 with 3 NPU cores. -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ----------------------------------------------------------------------- | @@ -2004,7 +2004,7 @@ The pre-trained YOLO-NAS weights from DeciAI are subject to their license and ca -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | -------------------------------------------------- | @@ -2044,7 +2044,7 @@ model: # required -Navigate to and configure: +Navigate to and, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ---------------------------------------------- | @@ -2138,7 +2138,7 @@ Once completed, configure the detector as follows: -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed. +Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed. @@ -2181,7 +2181,7 @@ It is also possible to eliminate the need for an AI server and run the hardware -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token. +Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token. @@ -2218,7 +2218,7 @@ If you do not possess whatever hardware you want to run, there's also the option -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token. +Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token. @@ -2274,7 +2274,7 @@ Use the model configuration shown below when using the axengine detector with th -Navigate to and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then navigate to and configure: +Navigate to and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: | Field | Value | | ---------------------------------------- | ----------------------- | diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index 9619f5a318..f112a0de96 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -204,8 +204,8 @@ You need to refer to **Configure hardware acceleration** above to enable the con -1. Navigate to and add a detector with **Type** `OpenVINO` and **Device** `GPU` -2. Navigate to and configure the model settings for OpenVINO: +1. Navigate to and add a detector with **Type** `OpenVINO` and **Device** `GPU` +2. On the same page, in the **Custom Model** tab, configure the model settings for OpenVINO: | Field | Value | | ---------------------------------------- | ------------------------------------------ | @@ -273,7 +273,7 @@ services: -Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`. +Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb`. diff --git a/docs/docs/integrations/plus.md b/docs/docs/integrations/plus.md index 9783cb212a..949a9f49be 100644 --- a/docs/docs/integrations/plus.md +++ b/docs/docs/integrations/plus.md @@ -3,6 +3,8 @@ id: plus title: Frigate+ --- +import NavPath from "@site/src/components/NavPath"; + For more information about how to use Frigate+ to improve your model, see the [Frigate+ docs](/plus/). :::info @@ -57,7 +59,7 @@ You can view all of your submitted images at [https://plus.frigate.video](https: Once you have [requested your first model](../plus/first_model.md) and gotten your own model ID, it can be used with a special model path. No other information needs to be configured for Frigate+ models because it fetches the remaining config from Frigate+ automatically. -You can either choose the new model from the Frigate+ pane in the Settings page of the Frigate UI, or manually set the model at the root level in your config: +You can either choose the new model from the pane in the Frigate UI (the **Frigate+ Model** tab), or manually set the model at the root level in your config: ```yaml detectors: ... diff --git a/docs/scripts/lib/nav_map.py b/docs/scripts/lib/nav_map.py index 80f13d65b7..0fddf40e00 100644 --- a/docs/scripts/lib/nav_map.py +++ b/docs/scripts/lib/nav_map.py @@ -63,8 +63,8 @@ SYSTEM_NAV: dict[str, tuple[str, str]] = { "environment_vars": ("System", "Environment variables"), "telemetry": ("System", "Telemetry"), "birdseye": ("System", "Birdseye"), - "detectors": ("System", "Detector hardware"), - "model": ("System", "Detection model"), + "detectors": ("System", "Detectors and model"), + "model": ("System", "Detectors and model"), } # All known top-level config section keys diff --git a/frigate/config/config.py b/frigate/config/config.py index 6873e6b880..04dd46a67a 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -80,12 +80,12 @@ logger = logging.getLogger(__name__) yaml = YAML() -DEFAULT_DETECTORS = { - "ov": { - "type": "openvino", - "device": "CPU", - } -} +# Pydantic field default applied when an existing config omits `detectors:`. +# Kept as cpu tflite for backwards compatibility with 0.17 configs. +DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} + +# Used by the openvino branch below and rendered into the new-config YAML +# template so first-time setups default to openvino on CPU. DEFAULT_MODEL = { "width": 300, "height": 300, @@ -94,6 +94,7 @@ DEFAULT_MODEL = { "path": "/openvino-model/ssdlite_mobilenet_v2.xml", "labelmap_path": "/openvino-model/coco_91cl_bkgr.txt", } +NEW_CONFIG_DETECTORS = {"ov": {"type": "openvino", "device": "CPU"}} DEFAULT_DETECT_DIMENSIONS = {"width": 1280, "height": 720} @@ -109,7 +110,7 @@ DEFAULT_CONFIG = f""" mqtt: enabled: False -{_render_default_yaml({"detectors": DEFAULT_DETECTORS, "model": DEFAULT_MODEL})} +{_render_default_yaml({"detectors": NEW_CONFIG_DETECTORS, "model": DEFAULT_MODEL})} cameras: {{}} # No cameras defined, UI wizard should be used version: {CURRENT_CONFIG_VERSION} """ diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index c63f27430a..48553465d3 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -64,9 +64,9 @@ class TestConfig(unittest.TestCase): def test_config_class(self): frigate_config = FrigateConfig(**self.minimal) - assert "ov" in frigate_config.detectors.keys() - assert frigate_config.detectors["ov"].type == DetectorTypeEnum.openvino - assert frigate_config.detectors["ov"].model.width == 300 + assert "cpu" in frigate_config.detectors.keys() + assert frigate_config.detectors["cpu"].type == DetectorTypeEnum.cpu + assert frigate_config.detectors["cpu"].model.width == 320 @patch("frigate.detectors.detector_config.load_labels") def test_detector_custom_model_path(self, mock_labels): diff --git a/web/e2e/specs/settings/detectors-and-model.spec.ts b/web/e2e/specs/settings/detectors-and-model.spec.ts new file mode 100644 index 0000000000..f697b2b2d6 --- /dev/null +++ b/web/e2e/specs/settings/detectors-and-model.spec.ts @@ -0,0 +1,55 @@ +/** + * Detectors and model settings page tests -- HIGH tier. + * + * Tests rendering of the merged page and navigation from the Frigate+ page. + */ + +import { test, expect } from "../../fixtures/frigate-test"; + +test.describe("Detectors and model Settings @high", () => { + test("page renders with detector and model cards", async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=systemDetectorsAndModel"); + await frigateApp.page.waitForTimeout(2000); + await expect(frigateApp.page.locator("#pageRoot")).toBeVisible(); + + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text).toContain("Detectors and model"); + expect(text?.toLowerCase()).toContain("detector hardware"); + expect(text?.toLowerCase()).toContain("detection model"); + }); + + test("Frigate+ page links to the merged page", async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=frigateplus"); + await frigateApp.page.waitForTimeout(2000); + + const button = frigateApp.page.getByRole("button", { + name: /Change in Detectors and model/, + }); + + // Button only appears when Frigate+ is enabled in the test config; skip + // the click assertion if it's not present. + if ((await button.count()) > 0) { + await button.first().click(); + await frigateApp.page.waitForURL(/page=systemDetectorsAndModel/); + await expect(frigateApp.page.locator("#pageRoot")).toContainText( + "Detectors and model", + ); + } else { + test.skip( + true, + "Frigate+ not enabled in this test config; skipping link assertion", + ); + } + }); + + test("old systemDetectionModel deep-link no longer routes here", async ({ + frigateApp, + }) => { + await frigateApp.goto("/settings?page=systemDetectionModel"); + await frigateApp.page.waitForTimeout(2000); + // The old page key is no longer in allSettingsViews; the router + // falls back to its default settings page (uiSettings). + const text = await frigateApp.page.textContent("#pageRoot"); + expect(text).not.toContain("Detection model"); + }); +}); diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 0f32d7e652..dd93a62ec0 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -12,6 +12,7 @@ "globalConfig": "Global Configuration - Frigate", "cameraConfig": "Camera Configuration - Frigate", "frigatePlus": "Frigate+ Settings - Frigate", + "detectorsAndModel": "Detectors and model - Frigate", "notifications": "Notification Settings - Frigate", "maintenance": "Maintenance - Frigate", "profiles": "Profiles - Frigate" @@ -69,8 +70,7 @@ "systemTelemetry": "Telemetry", "systemBirdseye": "Birdseye", "systemFfmpeg": "FFmpeg", - "systemDetectorHardware": "Detector hardware", - "systemDetectionModel": "Detection model", + "systemDetectorsAndModel": "Detectors and model", "systemMqtt": "MQTT", "systemGo2rtcStreams": "go2rtc streams", "integrationSemanticSearch": "Semantic search", @@ -1135,7 +1135,7 @@ "loading": "Loading model information…", "error": "Failed to load model information", "noModelLoaded": "No Frigate+ model is currently loaded.", - "availableModels": "Available Models", + "availableModels": "Available Frigate+ models", "loadingAvailableModels": "Loading available models…", "selectModel": "Select a model", "noModelsAvailable": "No models available", @@ -1146,6 +1146,7 @@ }, "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." }, + "changeInDetectorsAndModel": "Change model", "unsavedChanges": "Unsaved Frigate+ settings changes", "restart_required": "Restart required (Frigate+ model changed)", "toast": { @@ -1153,14 +1154,30 @@ "error": "Failed to save config changes: {{errorMessage}}" } }, - "detectionModel": { - "plusActive": { - "title": "Frigate+ model management", - "label": "Current model source", - "description": "This instance is running a Frigate+ model. Select or change your model in Frigate+ settings.", - "goToFrigatePlus": "Go to Frigate+ settings", - "showModelForm": "Manually configure a model" - } + "detectorsAndModel": { + "title": "Detectors and model", + "description": "Configure the detector backend that runs object detection and the model it uses. Changes are saved together so the detector and model stay in sync.", + "cardTitles": { + "detector": "Detector Hardware", + "model": "Detection Model" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Custom Model" + }, + "mismatch": { + "warning": "The current Frigate+ model \"{{model}}\" requires the {{required}} detector. Pick a compatible model below or switch to Custom Model before saving." + }, + "plusModel": { + "requiresDetector": "Requires: {{detector}}", + "noModelSelected": "Select a Frigate+ model" + }, + "toast": { + "saveSuccess": "Detectors and model settings saved. Restart Frigate to apply changes.", + "saveError": "Failed to save detector and model settings" + }, + "unsavedChanges": "Unsaved detector and model changes", + "restartRequired": "Restart required (detector or model changed)" }, "triggers": { "documentTitle": "Triggers", diff --git a/web/src/components/config-form/ConfigMessageBanner.tsx b/web/src/components/config-form/ConfigMessageBanner.tsx index f5b8280003..919d337c15 100644 --- a/web/src/components/config-form/ConfigMessageBanner.tsx +++ b/web/src/components/config-form/ConfigMessageBanner.tsx @@ -44,7 +44,7 @@ export function ConfigMessageBanner({ messages }: ConfigMessageBannerProps) { className="flex items-center [&>svg+div]:translate-y-0 [&>svg]:static [&>svg~*]:pl-2" > - {t(msg.messageKey)} + {t(msg.messageKey, msg.values)} ))} diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts index 9efeb2b32f..e3a89ba264 100644 --- a/web/src/components/config-form/section-configs/types.ts +++ b/web/src/components/config-form/section-configs/types.ts @@ -24,6 +24,8 @@ export type ConditionalMessage = { severity: MessageSeverity; /** Function returning true when the message should be shown */ condition: (ctx: MessageConditionContext) => boolean; + /** Optional interpolation values passed to t() for {{var}} substitution */ + values?: Record; }; /** Field-level conditional message, adds field targeting */ diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 6c32ffae6a..cd14047693 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -175,6 +175,9 @@ export interface BaseSectionProps { isSavingAll?: boolean; /** Callback when this section's saving state changes */ onSavingChange?: (isSaving: boolean) => void; + /** When true, render the form fields only; suppress the internal save/undo bar. + * The parent owns the save action and reads pending data via `onPendingDataChange`. */ + embedded?: boolean; } export interface CreateSectionOptions { @@ -211,6 +214,7 @@ export function ConfigSection({ onDeleteProfileSection, isSavingAll = false, onSavingChange, + embedded = false, }: ConfigSectionProps) { // For replay level, treat as camera-level config access const effectiveLevel = level === "replay" ? "camera" : level; @@ -1048,121 +1052,123 @@ export function ConfigSection({ }} /> -
+ {!embedded && (
- {hasChanges && ( -
- - {t("unsavedChanges", { - ns: "views/settings", - defaultValue: "You have unsaved changes", - })} - - -
- )} -
- {((effectiveLevel === "camera" && isOverridden) || - effectiveLevel === "global") && - !hasChanges && - !skipSave && - !profileName && ( - - )} - {profileName && - profileOverridesSection && - !hasChanges && - !skipSave && - onDeleteProfileSection && ( - - )} +
{hasChanges && ( +
+ + {t("unsavedChanges", { + ns: "views/settings", + defaultValue: "You have unsaved changes", + })} + + +
+ )} +
+ {((effectiveLevel === "camera" && isOverridden) || + effectiveLevel === "global") && + !hasChanges && + !skipSave && + !profileName && ( + + )} + {profileName && + profileOverridesSection && + !hasChanges && + !skipSave && + onDeleteProfileSection && ( + + )} + {hasChanges && ( + + )} - )} - +
-
+ )} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 42511e7a9c..b916e84609 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -44,7 +44,7 @@ import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; import MediaSyncSettingsView from "@/views/settings/MediaSyncSettingsView"; import RegionGridSettingsView from "@/views/settings/RegionGridSettingsView"; import Go2RtcStreamsSettingsView from "@/views/settings/Go2RtcStreamsSettingsView"; -import SystemDetectionModelSettingsView from "@/views/settings/SystemDetectionModelSettingsView"; +import DetectorsAndModelSettingsView from "@/views/settings/DetectorsAndModelSettingsView"; import { SingleSectionPage, type SettingsPageProps, @@ -127,8 +127,7 @@ const allSettingsViews = [ "systemEnvironmentVariables", "systemTelemetry", "systemBirdseye", - "systemDetectorHardware", - "systemDetectionModel", + "systemDetectorsAndModel", "systemMqtt", "systemGo2rtcStreams", "integrationSemanticSearch", @@ -229,11 +228,6 @@ const SystemEnvironmentVariablesSettingsPage = createSectionPage( ); const SystemTelemetrySettingsPage = createSectionPage("telemetry", "global"); const SystemBirdseyeSettingsPage = createSectionPage("birdseye", "global"); -const SystemDetectorHardwareSettingsPage = createSectionPage( - "detectors", - "global", -); -const SystemDetectionModelSettingsPage = SystemDetectionModelSettingsView; const NotificationsSettingsPage = createSectionPage("notifications", "global"); const SystemMqttSettingsPage = createSectionPage("mqtt", "global"); @@ -399,12 +393,8 @@ const settingsGroups = [ component: Go2RtcStreamsSettingsView, }, { - key: "systemDetectorHardware", - component: SystemDetectorHardwareSettingsPage, - }, - { - key: "systemDetectionModel", - component: SystemDetectionModelSettingsPage, + key: "systemDetectorsAndModel", + component: DetectorsAndModelSettingsView, }, { key: "systemDatabase", component: SystemDatabaseSettingsPage }, { key: "systemMqtt", component: SystemMqttSettingsPage }, @@ -558,8 +548,8 @@ const SYSTEM_SECTION_MAPPING: Record = { environment_vars: "systemEnvironmentVariables", telemetry: "systemTelemetry", birdseye: "systemBirdseye", - detectors: "systemDetectorHardware", - model: "systemDetectionModel", + detectors: "systemDetectorsAndModel", + model: "systemDetectorsAndModel", }; const CAMERA_SECTION_KEYS = new Set( diff --git a/web/src/views/settings/DetectorsAndModelSettingsView.tsx b/web/src/views/settings/DetectorsAndModelSettingsView.tsx new file mode 100644 index 0000000000..6780428da4 --- /dev/null +++ b/web/src/views/settings/DetectorsAndModelSettingsView.tsx @@ -0,0 +1,797 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { LuExternalLink, LuFilter } from "react-icons/lu"; +import { toast } from "sonner"; +import axios from "axios"; +import useSWR from "swr"; +import { useSWRConfig } from "swr"; +import { cn } from "@/lib/utils"; +import { useRestart } from "@/api/ws"; +import RestartDialog from "@/components/overlay/dialog/RestartDialog"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import Heading from "@/components/ui/heading"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Toaster } from "@/components/ui/sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, +} from "@/components/ui/select"; +import type { FrigateConfig } from "@/types/frigateConfig"; +import type { + SectionStatus, + SettingsPageProps, +} from "@/views/settings/SingleSectionPage"; +import type { ConfigSectionData } from "@/types/configForm"; +import { + SettingsGroupCard, + SplitCardRow, +} from "@/components/card/SettingsGroupCard"; +import { ConfigSectionTemplate } from "@/components/config-form/sections"; +import { ConfigMessageBanner } from "@/components/config-form/ConfigMessageBanner"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +type ModelTab = "plus" | "custom"; + +type PageState = { + detectors: ConfigSectionData; + modelTab: ModelTab; + plusModelId: string | undefined; + customModel: ConfigSectionData; +}; + +type FrigatePlusModel = { + id: string; + type: string; + name: string; + isBaseModel: boolean; + supportedDetectors: string[]; + trainDate: string; + baseModel: string; + width: number; + height: number; +}; + +const TYPE_MODEL_DEFAULTS: Record = { + cpu: { + path: "/cpu_model.tflite", + labelmap_path: "/labelmap.txt", + width: 320, + height: 320, + input_tensor: "nhwc", + input_pixel_format: "rgb", + input_dtype: "int", + model_type: "ssd", + }, + edgetpu: { + path: "/edgetpu_model.tflite", + labelmap_path: "/labelmap.txt", + width: 320, + height: 320, + input_tensor: "nhwc", + input_pixel_format: "rgb", + input_dtype: "int", + model_type: "ssd", + }, + openvino: { + path: "/openvino-model/ssdlite_mobilenet_v2.xml", + labelmap_path: "/openvino-model/coco_91cl_bkgr.txt", + width: 300, + height: 300, + input_tensor: "nhwc", + input_pixel_format: "bgr", + input_dtype: "int", + model_type: "ssd", + }, +}; + +const STATUS_BAR_KEY = "detectors_and_model"; + +const deriveInitialState = (config: FrigateConfig): PageState => { + const plusModelId = config.model?.plus?.id; + const modelPath = config.model?.path; + const plusEnabled = Boolean(config.plus?.enabled); + + // The reliable signal that a Plus model is currently active is the + // `model.plus.id` metadata + let modelTab: ModelTab; + if (plusModelId) { + modelTab = "plus"; + } else if (typeof modelPath === "string" && modelPath.length > 0) { + modelTab = "custom"; + } else if (plusEnabled) { + modelTab = "plus"; + } else { + modelTab = "custom"; + } + // Fallback: if Plus is not enabled, prefer Custom regardless of saved state + if (!plusEnabled && modelTab === "plus") { + modelTab = "custom"; + } + + const { plus: _plus, ...modelWithoutPlus } = (config.model ?? {}) as Record< + string, + unknown + >; + // If a Plus model is active, the resolved `model.path` is auto-derived from + // `plus.id` — drop it so the Custom tab starts clean and doesn't silently + // re-save the same Plus model when the user thinks they switched modes. + if (plusModelId) { + delete modelWithoutPlus.path; + } + + return { + detectors: (config.detectors ?? {}) as ConfigSectionData, + modelTab, + plusModelId: plusModelId ?? undefined, + customModel: modelWithoutPlus as ConfigSectionData, + }; +}; + +export default function DetectorsAndModelSettingsView({ + setUnsavedChanges, +}: SettingsPageProps) { + const { t } = useTranslation(["views/settings", "common"]); + const { getLocaleDocUrl } = useDocDomain(); + const { data: config } = useSWR("config"); + const { mutate: globalMutate } = useSWRConfig(); + const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; + + const [snapshot, setSnapshot] = useState(null); + const [state, setState] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [resetKey, setResetKey] = useState(0); + const [restartDialogOpen, setRestartDialogOpen] = useState(false); + const { send: sendRestart } = useRestart(); + const [childPending, setChildPending] = useState< + Record + >({}); + const [detectorStatus, setDetectorStatus] = useState({ + hasChanges: false, + isOverridden: false, + hasValidationErrors: false, + }); + const [modelStatus, setModelStatus] = useState({ + hasChanges: false, + isOverridden: false, + hasValidationErrors: false, + }); + + const [showBaseModels, setShowBaseModels] = useState(true); + const [showFineTunedModels, setShowFineTunedModels] = useState(true); + + const plusEnabled = Boolean(config?.plus?.enabled); + + const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR< + Record + >(plusEnabled ? "/plus/models" : null, { + fallbackData: {}, + fetcher: async (url) => { + const res = await axios.get(url, { withCredentials: true }); + return res.data.reduce( + (obj: Record, model: FrigatePlusModel) => { + obj[model.id] = model; + return obj; + }, + {}, + ); + }, + }); + + const filteredModelEntries = useMemo( + () => + Object.entries(availableModels || {}).filter(([, model]) => + model.isBaseModel ? showBaseModels : showFineTunedModels, + ), + [availableModels, showBaseModels, showFineTunedModels], + ); + + const isFilterActive = !showBaseModels || !showFineTunedModels; + + const currentDetectorType = useMemo(() => { + if (!state) return undefined; + const values = Object.values(state.detectors ?? {}); + if (values.length === 0) return undefined; + const first = values[0] as { type?: string } | undefined; + return first?.type; + }, [state]); + + // fill in defaults when detector type changes + const prevDetectorTypeRef = useRef(undefined); + useEffect(() => { + const newType = currentDetectorType; + const prevType = prevDetectorTypeRef.current; + prevDetectorTypeRef.current = newType; + if (prevType === undefined || prevType === newType) return; + if (!newType || !(newType in TYPE_MODEL_DEFAULTS)) return; + + const defaults = TYPE_MODEL_DEFAULTS[newType]; + setChildPending((prev) => { + const next: Record = { + ...prev, + model: defaults, + }; + if (newType === "openvino") { + const detectorsCurrent = (prev.detectors ?? state?.detectors ?? {}) as { + [key: string]: { device?: string }; + }; + const entries = Object.entries(detectorsCurrent); + if (entries.length > 0) { + const [firstKey, firstValue] = entries[0]; + if (!firstValue?.device) { + next.detectors = { + ...detectorsCurrent, + [firstKey]: { ...firstValue, device: "CPU" }, + } as ConfigSectionData; + } + } + } + return next; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentDetectorType]); + + const isModelCompatible = useCallback( + (model: FrigatePlusModel) => + currentDetectorType + ? model.supportedDetectors.includes(currentDetectorType) + : true, + [currentDetectorType], + ); + + const selectedPlusModel = state?.plusModelId + ? availableModels?.[state.plusModelId] + : undefined; + + const plusMismatch = + state?.modelTab === "plus" && + selectedPlusModel !== undefined && + currentDetectorType !== undefined && + !isModelCompatible(selectedPlusModel); + + const plusModelMissing = state?.modelTab === "plus" && !state?.plusModelId; + + const handleChildPendingChange = useCallback( + ( + sectionKey: string, + _cameraName: string | undefined, + data: ConfigSectionData | null, + ) => { + setChildPending((prev) => { + if (data === null) { + if (!(sectionKey in prev)) return prev; + const { [sectionKey]: _drop, ...rest } = prev; + return rest; + } + return { ...prev, [sectionKey]: data }; + }); + }, + [], + ); + + const handleDetectorStatusChange = useCallback( + (status: SectionStatus) => setDetectorStatus(status), + [], + ); + + const handleModelStatusChange = useCallback( + (status: SectionStatus) => setModelStatus(status), + [], + ); + + useEffect(() => { + const detectorsPending = childPending["detectors"]; + setState((prev) => { + if (!prev || !snapshot) return prev; + // When the embedded form un-modifies (data returns to baseline) it clears + // its entry from childPending — fall back to snapshot so state.detectors + // doesn't keep a stale value the user has visually reverted. + return { + ...prev, + detectors: detectorsPending ?? snapshot.detectors, + }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childPending["detectors"]]); + + useEffect(() => { + const modelPending = childPending["model"]; + setState((prev) => { + if (!prev || !snapshot) return prev; + return { + ...prev, + customModel: modelPending ?? snapshot.customModel, + }; + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [childPending["model"]]); + + useEffect(() => { + if (!config || snapshot !== null) return; + const initial = deriveInitialState(config); + setSnapshot(initial); + setState(initial); + }, [config, snapshot]); + + const isDirty = useMemo(() => { + if (!state || !snapshot) return false; + return JSON.stringify(state) !== JSON.stringify(snapshot); + }, [state, snapshot]); + + useEffect(() => { + if (isDirty) { + addMessage( + STATUS_BAR_KEY, + t("detectorsAndModel.unsavedChanges"), + undefined, + STATUS_BAR_KEY, + ); + } else { + removeMessage(STATUS_BAR_KEY, STATUS_BAR_KEY); + } + setUnsavedChanges?.(isDirty); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isDirty]); + + useEffect(() => { + document.title = t("documentTitle.detectorsAndModel"); + }, [t]); + + const onSave = useCallback(async () => { + if (!state || !snapshot) return; + + const tabChanged = state.modelTab !== snapshot.modelTab; + + const modelPayload = + state.modelTab === "plus" + ? { path: `plus://${state.plusModelId}` } + : state.customModel; + + const detectorKeysChanged = + JSON.stringify(Object.keys(state.detectors).sort()) !== + JSON.stringify(Object.keys(snapshot.detectors).sort()); + + setIsSaving(true); + try { + if (tabChanged) { + // Best-effort cleanup of the prior model's fields + try { + await axios.put("config/set", { + requires_restart: 0, + config_data: { model: null }, + }); + } catch { + // intentional no-op — see comment above + } + } + + if (detectorKeysChanged) { + // Best-effort cleanup + try { + await axios.put("config/set", { + requires_restart: 0, + config_data: { detectors: null }, + }); + } catch { + // intentional no-op — see comment above + } + } + + await axios.put("config/set", { + requires_restart: 0, + config_data: { + detectors: state.detectors, + model: modelPayload, + }, + }); + + await globalMutate("config"); + await globalMutate("config/raw_paths"); + + // Re-derive snapshot from the freshly saved state so isDirty resets. + setSnapshot({ ...state }); + setChildPending({}); + setResetKey((k) => k + 1); + + addMessage( + "detectors_and_model_restart", + t("detectorsAndModel.restartRequired"), + undefined, + "detectors_and_model_restart", + ); + + toast.success(t("detectorsAndModel.toast.saveSuccess"), { + position: "top-center", + action: ( + + ), + }); + } catch (error) { + const err = error as { + response?: { data?: { message?: string; detail?: string } }; + }; + const message = + err.response?.data?.message || + err.response?.data?.detail || + t("detectorsAndModel.toast.saveError"); + toast.error(message, { position: "top-center" }); + // Re-sync the config cache in case the two-step PUT left the backend + // ahead of the frontend (e.g. step 1 cleared `model` but step 2 failed). + await globalMutate("config"); + } finally { + setIsSaving(false); + } + }, [state, snapshot, globalMutate, addMessage, t]); + + const onUndo = useCallback(() => { + if (snapshot) { + setState(snapshot); + setChildPending({}); + // Force the embedded forms to re-mount so their internal dirty/baseline + // state is rebuilt from the current config — clearing childPending alone + // doesn't reset BaseSection's internal tracking. + setResetKey((k) => k + 1); + } + }, [snapshot]); + + if (!config || !state) { + return ; + } + + const saveDisabled = + !isDirty || + isSaving || + detectorStatus.hasValidationErrors || + (state.modelTab === "custom" && modelStatus.hasValidationErrors) || + plusMismatch || + plusModelMissing; + + return ( +
+ +
+
+ {t("detectorsAndModel.title")} +
+ {t("detectorsAndModel.description")} +
+
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+ {isDirty && ( + + {t("button.modified", { ns: "common", defaultValue: "Modified" })} + + )} +
+
+
+ + + + {plusMismatch && selectedPlusModel && ( + true, + values: { + model: selectedPlusModel.name, + required: selectedPlusModel.supportedDetectors.join(", "), + }, + }, + ]} + /> + )} + + {plusEnabled ? ( + + setState((prev) => + prev ? { ...prev, modelTab: value as ModelTab } : prev, + ) + } + > + + + {t("detectorsAndModel.tabs.plus")} + + + {t("detectorsAndModel.tabs.custom")} + + + + + + frigatePlus.modelInfo.modelSelect + + } + content={ +
+ + + + + + +
+
+ {t("frigatePlus.modelInfo.filter.ariaLabel")} +
+
+ + +
+
+ + +
+
+
+
+
+ } + /> +
+ + + + +
+ ) : ( + + )} +
+
+
+ +
+
+ {isDirty && ( + + {t("unsavedChanges", { ns: "views/settings" })} + + )} +
+ {isDirty && ( + + )} + +
+
+
+ setRestartDialogOpen(false)} + onRestart={() => sendRestart("restart")} + /> +
+ ); +} diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 7bc81fae22..806bc61839 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -1,234 +1,28 @@ -import Heading from "@/components/ui/heading"; -import { useCallback, useContext, useEffect, useMemo, useState } from "react"; +import { useEffect } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import useSWR from "swr"; +import { CheckCircle2, XCircle } from "lucide-react"; +import { LuExternalLink } from "react-icons/lu"; import { Toaster } from "@/components/ui/sonner"; import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { toast } from "sonner"; -import useSWR from "swr"; -import axios from "axios"; -import { FrigateConfig } from "@/types/frigateConfig"; -import { CheckCircle2, XCircle } from "lucide-react"; -import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; -import { Link } from "react-router-dom"; -import { LuExternalLink, LuFilter } from "react-icons/lu"; -import { StatusBarMessagesContext } from "@/context/statusbar-provider"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, -} from "@/components/ui/select"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Switch } from "@/components/ui/switch"; -import { Label } from "@/components/ui/label"; -import { cn } from "@/lib/utils"; -import { useDocDomain } from "@/hooks/use-doc-domain"; -import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import Heading from "@/components/ui/heading"; import { SettingsGroupCard, SplitCardRow, } from "@/components/card/SettingsGroupCard"; import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigatePlusCurrentModelSummary"; -import { useRestart } from "@/api/ws"; -import RestartDialog from "@/components/overlay/dialog/RestartDialog"; +import { useDocDomain } from "@/hooks/use-doc-domain"; +import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import { FrigateConfig } from "@/types/frigateConfig"; +import type { SettingsPageProps } from "@/views/settings/SingleSectionPage"; -type FrigatePlusModel = { - id: string; - type: string; - name: string; - isBaseModel: boolean; - supportedDetectors: string[]; - trainDate: string; - baseModel: string; - width: number; - height: number; -}; - -type FrigatePlusSettings = { - model: { - id?: string; - }; -}; - -type FrigateSettingsViewProps = { - setUnsavedChanges: React.Dispatch>; -}; - -export default function FrigatePlusSettingsView({ - setUnsavedChanges, -}: FrigateSettingsViewProps) { +export default function FrigatePlusSettingsView(_props: SettingsPageProps) { const { t } = useTranslation("views/settings"); const { getLocaleDocUrl } = useDocDomain(); - const { data: config, mutate: updateConfig } = - useSWR("config"); - const [changedValue, setChangedValue] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [restartDialogOpen, setRestartDialogOpen] = useState(false); - const { send: sendRestart } = useRestart(); - - const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; - - const [frigatePlusSettings, setFrigatePlusSettings] = - useState({ - model: { - id: undefined, - }, - }); - - const [origPlusSettings, setOrigPlusSettings] = useState( - { - model: { - id: undefined, - }, - }, - ); - - const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR< - Record - >("/plus/models", { - fallbackData: {}, - fetcher: async (url) => { - const res = await axios.get(url, { withCredentials: true }); - return res.data.reduce( - (obj: Record, model: FrigatePlusModel) => { - obj[model.id] = model; - return obj; - }, - {}, - ); - }, - }); - - const [showBaseModels, setShowBaseModels] = useState(true); - const [showFineTunedModels, setShowFineTunedModels] = useState(true); - - const filteredModelEntries = useMemo( - () => - Object.entries(availableModels || {}).filter(([, model]) => - model.isBaseModel ? showBaseModels : showFineTunedModels, - ), - [availableModels, showBaseModels, showFineTunedModels], - ); - - const isFilterActive = !showBaseModels || !showFineTunedModels; - - useEffect(() => { - if (config) { - if (frigatePlusSettings?.model.id == undefined) { - setFrigatePlusSettings({ - model: { - id: config.model.plus?.id, - }, - }); - } - - setOrigPlusSettings({ - model: { - id: config.model.plus?.id, - }, - }); - } - // we know that these deps are correct - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [config]); - - const handleFrigatePlusConfigChange = ( - newConfig: Partial, - ) => { - setFrigatePlusSettings((prevConfig) => ({ - model: { - ...prevConfig.model, - ...newConfig.model, - }, - })); - setUnsavedChanges(true); - setChangedValue(true); - }; - - const saveToConfig = useCallback(async () => { - setIsLoading(true); - - try { - // Clear the existing model section so only the new path remains - await axios.put("config/set", { - requires_restart: 0, - config_data: { model: null }, - }); - const res = await axios.put("config/set", { - requires_restart: 0, - config_data: { - model: { path: `plus://${frigatePlusSettings.model.id}` }, - }, - }); - - if (res.status === 200) { - toast.success(t("frigatePlus.toast.success"), { - position: "top-center", - action: ( - setRestartDialogOpen(true)}> - - - ), - }); - setChangedValue(false); - updateConfig(); - } else { - toast.error( - t("frigatePlus.toast.error", { errorMessage: res.statusText }), - { - position: "top-center", - }, - ); - } - } catch (error) { - const err = error as { - response?: { data?: { message?: string; detail?: string } }; - }; - const errorMessage = - err.response?.data?.message || - err.response?.data?.detail || - "Unknown error"; - toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), { - position: "top-center", - }); - } finally { - addMessage( - "plus_restart", - t("frigatePlus.restart_required"), - undefined, - "plus_restart", - ); - setIsLoading(false); - } - }, [updateConfig, addMessage, frigatePlusSettings, t]); - - const onCancel = useCallback(() => { - setFrigatePlusSettings(origPlusSettings); - setChangedValue(false); - removeMessage("plus_settings", "plus_settings"); - }, [origPlusSettings, removeMessage]); - - useEffect(() => { - if (changedValue) { - addMessage( - "plus_settings", - t("frigatePlus.unsavedChanges"), - undefined, - "plus_settings", - ); - } else { - removeMessage("plus_settings", "plus_settings"); - } - // we know that these deps are correct - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [changedValue]); + const { data: config } = useSWR("config"); + const navigate = useNavigate(); useEffect(() => { document.title = t("documentTitle.frigatePlus"); @@ -246,7 +40,6 @@ export default function FrigatePlusSettingsView({ {t("frigatePlus.title")} -

{t("frigatePlus.description")}

@@ -292,170 +85,20 @@ export default function FrigatePlusSettingsView({ {config?.plus?.enabled && ( - - )} - - {config?.plus?.enabled && ( - - - frigatePlus.modelInfo.modelSelect - - } - content={ -
- - - - - - -
-
- {t("frigatePlus.modelInfo.filter.ariaLabel")} -
-
- - -
-
- - -
-
-
-
-
- } - /> -
+ + navigate("/settings?page=systemDetectorsAndModel") + } + > + {t("frigatePlus.changeInDetectorsAndModel")} + + } + /> )} @@ -524,55 +167,6 @@ export default function FrigatePlusSettingsView({
- -
-
- {changedValue && ( -
- - {t("unsavedChanges")} - -
- )} -
- {changedValue && ( - - )} - -
-
-
- setRestartDialogOpen(false)} - onRestart={() => sendRestart("restart")} - /> ); } diff --git a/web/src/views/settings/SystemDetectionModelSettingsView.tsx b/web/src/views/settings/SystemDetectionModelSettingsView.tsx deleted file mode 100644 index 7038378a20..0000000000 --- a/web/src/views/settings/SystemDetectionModelSettingsView.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import Heading from "@/components/ui/heading"; -import { Button } from "@/components/ui/button"; -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import useSWR from "swr"; -import type { FrigateConfig } from "@/types/frigateConfig"; -import { - SettingsGroupCard, - SplitCardRow, -} from "@/components/card/SettingsGroupCard"; -import { - SingleSectionPage, - type SettingsPageProps, -} from "@/views/settings/SingleSectionPage"; -import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigatePlusCurrentModelSummary"; -import { useTranslation } from "react-i18next"; - -export default function SystemDetectionModelSettingsView( - props: SettingsPageProps, -) { - const { t } = useTranslation(["config/global", "views/settings"]); - const { data: config } = useSWR("config"); - const [showModelForm, setShowModelForm] = useState(false); - const navigate = useNavigate(); - - if (!config) { - return ; - } - - const isPlusModelActive = Boolean(config?.model?.plus?.id); - - if (!isPlusModelActive || showModelForm) { - return ; - } - - return ( -
-
-
- {t("model.label", { ns: "config/global" })} -
- {t("model.description", { ns: "config/global" })} -
-
-
- -
- - - - -
- } - /> - - - -
- - ); -} diff --git a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx index 8ff4bde305..3db973f2e9 100644 --- a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx +++ b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from "react"; import { SettingsGroupCard, SplitCardRow, @@ -7,15 +8,26 @@ import { useTranslation } from "react-i18next"; type FrigatePlusCurrentModelSummaryProps = { plusModel: FrigateConfig["model"]["plus"]; + action?: ReactNode; }; export default function FrigatePlusCurrentModelSummary({ plusModel, + action, }: FrigatePlusCurrentModelSummaryProps) { const { t } = useTranslation("views/settings"); + const title = action ? ( +
+ {t("frigatePlus.cardTitles.currentModel")} + {action} +
+ ) : ( + t("frigatePlus.cardTitles.currentModel") + ); + return ( - + {!plusModel && (

{t("frigatePlus.modelInfo.noModelLoaded")} From 32daf6f494e7a97f3043ad9e51abc234132586ac Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 17 May 2026 15:40:33 -0500 Subject: [PATCH 02/94] Miscellaneous fixes (#23217) * fix hardcoded leading-slash hrefs to respect FRIGATE_BASE_PATH * update docs for default detector --- docs/docs/configuration/object_detectors.md | 2 +- web/src/components/auth/ProtectedRoute.tsx | 3 ++- web/src/components/card/ReviewCard.tsx | 6 +++++- web/src/components/chat/ChatAttachmentChip.tsx | 3 ++- web/src/components/chat/ChatEventThumbnailsRow.tsx | 3 ++- web/src/components/menu/SearchResultActions.tsx | 6 +++++- web/src/components/overlay/DebugReplayDialog.tsx | 7 ++++++- web/src/components/overlay/ExportDialog.tsx | 6 +++++- web/src/components/overlay/MobileReviewSettingsDrawer.tsx | 7 ++++++- web/src/components/timeline/EventMenu.tsx | 7 ++++++- web/src/views/events/EventView.tsx | 7 ++++++- web/src/views/motion-search/MotionSearchView.tsx | 7 ++++++- 12 files changed, 52 insertions(+), 12 deletions(-) diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index b1baf690c0..21f86798cb 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -72,7 +72,7 @@ This does not affect using hardware for accelerating other tasks such as [semant # Officially Supported Detectors -Frigate provides a number of builtin detector types. By default, Frigate will use a single OpenVINO detector running on the CPU. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. +Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. ## Edge TPU Detector diff --git a/web/src/components/auth/ProtectedRoute.tsx b/web/src/components/auth/ProtectedRoute.tsx index bcfa8fdf36..c10a94a020 100644 --- a/web/src/components/auth/ProtectedRoute.tsx +++ b/web/src/components/auth/ProtectedRoute.tsx @@ -6,6 +6,7 @@ import { isRedirectingToLogin, setRedirectingToLogin, } from "@/api/auth-redirect"; +import { baseUrl } from "@/api/baseUrl"; export default function ProtectedRoute({ requiredRoles, @@ -24,7 +25,7 @@ export default function ProtectedRoute({ !isRedirectingToLogin() ) { setRedirectingToLogin(true); - window.location.href = "/login"; + window.location.href = `${baseUrl}login`; } }, [auth.isLoading, auth.isAuthenticated, auth.user]); diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index 33d651ed89..19b4714095 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -94,7 +94,11 @@ export default function ReviewCard({ toast.success(t("export.toast.success"), { position: "top-center", action: ( - + ), diff --git a/web/src/components/chat/ChatAttachmentChip.tsx b/web/src/components/chat/ChatAttachmentChip.tsx index 5894efaa77..ef4a114555 100644 --- a/web/src/components/chat/ChatAttachmentChip.tsx +++ b/web/src/components/chat/ChatAttachmentChip.tsx @@ -1,4 +1,5 @@ import { useApiHost } from "@/api"; +import { baseUrl } from "@/api/baseUrl"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; @@ -79,7 +80,7 @@ export function ChatAttachmentChip({ e.stopPropagation()} diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index b88e698531..43a192dc9e 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -116,7 +116,11 @@ export default function SearchResultActions({ closeButton: true, dismissible: false, action: ( - + diff --git a/web/src/components/overlay/DebugReplayDialog.tsx b/web/src/components/overlay/DebugReplayDialog.tsx index 2f9a7159a6..665040dc5c 100644 --- a/web/src/components/overlay/DebugReplayDialog.tsx +++ b/web/src/components/overlay/DebugReplayDialog.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from "react"; +import { baseUrl } from "@/api/baseUrl"; import { Dialog, DialogContent, @@ -227,7 +228,11 @@ export default function DebugReplayDialog({ closeButton: true, dismissible: false, action: ( - + ), diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index eeec65f962..0d57821fc1 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -163,7 +163,11 @@ export default function ExportDialog({ toast.success(t("export.toast.queued"), { position: "top-center", action: ( - + ), diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index bca921072e..2ad2067462 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from "react"; +import { baseUrl } from "@/api/baseUrl"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; @@ -190,7 +191,11 @@ export default function MobileReviewSettingsDrawer({ toast.success(t("export.toast.queued", { ns: "components/dialog" }), { position: "top-center", action: ( - + diff --git a/web/src/components/timeline/EventMenu.tsx b/web/src/components/timeline/EventMenu.tsx index 375430c2e9..42efd2c97f 100644 --- a/web/src/components/timeline/EventMenu.tsx +++ b/web/src/components/timeline/EventMenu.tsx @@ -8,6 +8,7 @@ import { } from "@/components/ui/dropdown-menu"; import { HiDotsHorizontal } from "react-icons/hi"; import { useApiHost } from "@/api"; +import { baseUrl } from "@/api/baseUrl"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Event } from "@/types/event"; @@ -79,7 +80,11 @@ export default function EventMenu({ closeButton: true, dismissible: false, action: ( - + diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index edf144f883..5ffbe5f960 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -49,6 +49,7 @@ import { FiMoreVertical } from "react-icons/fi"; import { IoMdArrowRoundBack } from "react-icons/io"; import useSWR from "swr"; import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; +import { baseUrl } from "@/api/baseUrl"; import { Button } from "@/components/ui/button"; import BlurredIconButton from "@/components/button/BlurredIconButton"; import { @@ -284,7 +285,11 @@ export default function EventView({ { position: "top-center", action: ( - + diff --git a/web/src/views/motion-search/MotionSearchView.tsx b/web/src/views/motion-search/MotionSearchView.tsx index 97811b7402..8a5ce680d7 100644 --- a/web/src/views/motion-search/MotionSearchView.tsx +++ b/web/src/views/motion-search/MotionSearchView.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import useSWR from "swr"; import axios from "axios"; import { isDesktop, isMobile } from "react-device-detect"; +import { baseUrl } from "@/api/baseUrl"; import Logo from "@/components/Logo"; import { FrigateConfig } from "@/types/frigateConfig"; import { TimeRange } from "@/types/timeline"; @@ -363,7 +364,11 @@ export default function MotionSearchView({ { position: "top-center", action: ( - + From 620923c27e7b6a6c07a39e904c42112686a124f7 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 May 2026 08:49:25 -0500 Subject: [PATCH 03/94] clear both detector and model together (#23232) --- .../DetectorsAndModelSettingsView.tsx | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/web/src/views/settings/DetectorsAndModelSettingsView.tsx b/web/src/views/settings/DetectorsAndModelSettingsView.tsx index 6780428da4..bb18ed2777 100644 --- a/web/src/views/settings/DetectorsAndModelSettingsView.tsx +++ b/web/src/views/settings/DetectorsAndModelSettingsView.tsx @@ -373,27 +373,15 @@ export default function DetectorsAndModelSettingsView({ setIsSaving(true); try { - if (tabChanged) { - // Best-effort cleanup of the prior model's fields + // Pre-clear both `detectors` and `model` together when a renaming + if (tabChanged || detectorKeysChanged) { try { await axios.put("config/set", { requires_restart: 0, - config_data: { model: null }, + config_data: { detectors: null, model: null }, }); } catch { - // intentional no-op — see comment above - } - } - - if (detectorKeysChanged) { - // Best-effort cleanup - try { - await axios.put("config/set", { - requires_restart: 0, - config_data: { detectors: null }, - }); - } catch { - // intentional no-op — see comment above + // best-effort cleanup } } From d968f0050038b0c6b43fc531e2f244bb2ed3e324 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 May 2026 14:22:54 -0500 Subject: [PATCH 04/94] Settings UI fixes (#23237) * detector UI fixes - derive detector and model from memo rather than using two drain useeffects - sanitize save payload through sanitizeSectionData to prevent yaml validation issues * increase display duration for restart required toasts * mimic logic in detector section for save all button also, increase toast duration for restart required toasts * fixes and tweaks - use section hidden fields for sanitization instead of duplicating code - use parent hooks so save all, pending data, and the status dots work correctly --- web/public/locales/en/views/settings.json | 2 + .../config-form/sections/BaseSection.tsx | 1 + web/src/pages/Settings.tsx | 171 +++++++-- .../DetectorsAndModelSettingsView.tsx | 333 ++++++++++++------ 4 files changed, 381 insertions(+), 126 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index dd93a62ec0..0255082f1f 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1583,6 +1583,8 @@ "resetError": "Failed to reset settings", "saveAllSuccess_one": "Saved {{count}} section successfully.", "saveAllSuccess_other": "All {{count}} sections saved successfully.", + "saveAllSuccessRestartRequired_one": "Saved {{count}} section successfully. Restart Frigate to apply your changes.", + "saveAllSuccessRestartRequired_other": "All {{count}} sections saved successfully. Restart Frigate to apply your changes.", "saveAllPartial_one": "{{successCount}} of {{totalCount}} section saved. {{failCount}} failed.", "saveAllPartial_other": "{{successCount}} of {{totalCount}} sections saved. {{failCount}} failed.", "saveAllFailure": "Failed to save all sections." diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index cd14047693..9245fa2409 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -743,6 +743,7 @@ export function ConfigSection({ "Settings saved successfully. Restart Frigate to apply your changes.", }), { + duration: 10000, action: ( setRestartDialogOpen(true)}> - )} - +

+
+ {t("go2rtcStreams.streamNumber", { index: urlIndex + 1 })} {canRemove && ( )}
- - {/* ffmpeg module toggle */} -
- - -
- - {/* ffmpeg options */} - {parsed.isFfmpeg && ( -
- {/* Video */} -
- - -
- - {/* Audio */} -
- - -
- - {/* Hardware acceleration - only when transcoding video */} - {isTranscodingVideo && ( -
- - handleBaseUrlChange(e.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={t("go2rtcStreams.streamUrlPlaceholder")} + /> + {canToggleCredentials && ( +
- )} + {showCredentials || isFocused ? ( + + ) : ( + + )} + + )} +
+ + + + + + {t("go2rtcStreams.ffmpeg.useFfmpegModule")} + +
- )} + + {/* ffmpeg options */} + {parsed.isFfmpeg && ( +
+ {/* Video — one row per #video= fragment */} +
+
+ + {parsed.videos[0] !== "exclude" && ( + + )} +
+ {parsed.videos.map((v, idx) => ( +
+ + {idx > 0 ? ( + + ) : ( + // Reserve the same horizontal slot so the primary Select + // doesn't stretch wider than fallback rows. + + ))} +
+ + {/* Audio — one row per #audio= fragment */} +
+
+ + {parsed.audios[0] !== "exclude" && ( + + )} +
+ {parsed.audios.map((a, idx) => ( +
+ + {idx > 0 ? ( + + ) : ( + + ))} +
+ + {/* Hardware acceleration — only when transcoding video */} + {isTranscodingVideo && ( +
+
+ +
+ +
+ )} +
+ )} +
); } From b1de5e22909ae393c520a2da04d437f6533828fa Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 19 May 2026 09:31:50 -0500 Subject: [PATCH 08/94] Add attributes to UI filters list (#23250) * preserve user-set min_score on attribute filters instead of bumping any 0.5 value use model_fields_set to distinguish "user explicitly set min_score" from "Pydantic applied the generic FilterConfig default of 0.5" * add config test for attributes * fix attributes frontend type * add expanded hidden field context * extend schema modification * special case for attributes * i18n for attributes * handle dedicated lpr mode * strip unrendered FilterConfig fields from attribute filter form data to fix validation errors --- frigate/config/config.py | 7 +- frigate/test/test_config.py | 55 ++++++ generate_config_translations.py | 58 ++++++ web/public/locales/en/config/cameras.json | 2 +- web/public/locales/en/config/global.json | 37 +++- .../config-form/section-configs/objects.ts | 60 ++++++- .../config-form/sections/BaseSection.tsx | 43 ++++- .../sections/CameraOverridesBadge.tsx | 3 +- .../sections/section-special-cases.ts | 165 +++++++++++++++++- .../config-form/theme/utils/i18n.ts | 22 ++- web/src/hooks/use-config-override.ts | 13 +- web/src/pages/Settings.tsx | 5 +- web/src/types/configForm.ts | 14 +- web/src/types/frigateConfig.ts | 4 +- web/src/utils/configUtil.ts | 85 +++++++-- .../DetectorsAndModelSettingsView.tsx | 5 +- 16 files changed, 535 insertions(+), 43 deletions(-) diff --git a/frigate/config/config.py b/frigate/config/config.py index 04dd46a67a..7aa6dac59d 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -629,10 +629,11 @@ class FrigateConfig(FrigateBaseModel): # set default min_score for object attributes for attribute in self.model.all_attributes: - if not self.objects.filters.get(attribute): + existing = self.objects.filters.get(attribute) + if existing is None: self.objects.filters[attribute] = FilterConfig(min_score=0.7) - elif self.objects.filters[attribute].min_score == 0.5: - self.objects.filters[attribute].min_score = 0.7 + elif "min_score" not in existing.model_fields_set: + existing.min_score = 0.7 # auto detect hwaccel args if self.ffmpeg.hwaccel_args == "auto": diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 48553465d3..6490a65099 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -1673,5 +1673,60 @@ class TestConfig(unittest.TestCase): self.assertRaises(ValueError, lambda: FrigateConfig(**config)) +class TestAttributeFilterDefaults(unittest.TestCase): + """Verify attribute filter min_score handling at config load.""" + + def setUp(self): + self.minimal = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + } + }, + } + + def _build_config(self, object_filters: dict | None = None) -> FrigateConfig: + config = deep_merge({}, self.minimal) + if object_filters is not None: + config.setdefault("objects", {})["filters"] = object_filters + return FrigateConfig(**config) + + def test_attribute_with_no_filter_gets_default_min_score(self): + """Attribute with no user-provided filter gets created with min_score=0.7.""" + config = self._build_config() + face_filter = config.objects.filters.get("face") + self.assertIsNotNone(face_filter) + self.assertEqual(face_filter.min_score, 0.7) + + def test_attribute_filter_without_min_score_gets_bumped(self): + """If user sets some FilterConfig field but not min_score, min_score is bumped to 0.7.""" + config = self._build_config({"face": {"min_area": 500}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_area, 500) + self.assertEqual(face_filter.min_score, 0.7) + + def test_attribute_filter_explicit_min_score_half_is_preserved(self): + """User-provided min_score=0.5 must NOT be silently rewritten to 0.7.""" + config = self._build_config({"face": {"min_score": 0.5}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_score, 0.5) + + def test_attribute_filter_explicit_min_score_other_value_is_preserved(self): + """Sanity: explicit non-0.5 values pass through unchanged.""" + config = self._build_config({"face": {"min_score": 0.3}}) + face_filter = config.objects.filters["face"] + self.assertEqual(face_filter.min_score, 0.3) + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/generate_config_translations.py b/generate_config_translations.py index 9bc830855d..7f9c9bc504 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -364,6 +364,64 @@ def main(): continue section_data.pop(key, None) + if field_name == "objects": + # Produce a parallel `filters_attribute` block alongside `filters`, + # with object-wording rewritten for attribute filters (face, + # license_plate, courier logos). The frontend's + # buildTranslationPath routes `filters..` lookups to + # `filters_attribute.` when `` is in + # `model.all_attributes`. Keep this rewrite list explicit rather + # than running a blanket s/object/attribute/ so unrelated + # descriptions (e.g. "JSON object") never accidentally flip. + filters_block = section_data.get("filters") + if isinstance(filters_block, dict): + attribute_rewrites = [ + ("Object filters", "Attribute filters"), + ("detected objects", "detected attributes"), + ("object area", "attribute area"), + ("object type", "attribute"), + ("the object", "the attribute"), + ] + + # Per-field overrides for cases where the generic rewrite + # doesn't capture the attribute-specific semantics. Keys + # match the FilterConfig field name; values are partial + # overrides applied AFTER the generic rewrites. + attribute_field_overrides: Dict[str, Dict[str, str]] = { + "min_score": { + "description": ( + "Minimum single-frame detection confidence required " + "to associate this attribute with its parent object." + ), + }, + } + + def rewrite(text: str) -> str: + for source, replacement in attribute_rewrites: + text = text.replace(source, replacement) + return text + + attribute_variant: Dict[str, Any] = {} + for key, value in filters_block.items(): + if key in ("label", "description"): + if isinstance(value, str): + attribute_variant[key] = rewrite(value) + continue + if not isinstance(value, dict): + continue + field_trans: Dict[str, str] = {} + if isinstance(value.get("label"), str): + field_trans["label"] = rewrite(value["label"]) + if isinstance(value.get("description"), str): + field_trans["description"] = rewrite(value["description"]) + overrides = attribute_field_overrides.get(key) + if overrides: + field_trans.update(overrides) + if field_trans: + attribute_variant[key] = field_trans + if attribute_variant: + section_data["filters_attribute"] = attribute_variant + if not section_data: logger.warning(f"No translations found for section: {field_name}") continue diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index f645dd33a1..4f2c0ea01e 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -950,4 +950,4 @@ "label": "Original camera state", "description": "Keep track of original state of camera." } -} \ No newline at end of file +} diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index b10f0a7afc..1f5c39248c 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -921,6 +921,41 @@ "label": "Original GenAI state", "description": "Indicates whether GenAI was enabled in the original static config." } + }, + "filters_attribute": { + "label": "Attribute filters", + "description": "Filters applied to detected attributes to reduce false positives (area, ratio, confidence).", + "min_area": { + "label": "Minimum attribute area", + "description": "Minimum bounding box area (pixels or percentage) required for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "max_area": { + "label": "Maximum attribute area", + "description": "Maximum bounding box area (pixels or percentage) allowed for this attribute. Can be pixels (int) or percentage (float between 0.000001 and 0.99)." + }, + "min_ratio": { + "label": "Minimum aspect ratio", + "description": "Minimum width/height ratio required for the bounding box to qualify." + }, + "max_ratio": { + "label": "Maximum aspect ratio", + "description": "Maximum width/height ratio allowed for the bounding box to qualify." + }, + "threshold": { + "label": "Confidence threshold", + "description": "Average detection confidence threshold required for the attribute to be considered a true positive." + }, + "min_score": { + "label": "Minimum confidence", + "description": "Minimum single-frame detection confidence required to associate this attribute with its parent object." + }, + "mask": { + "label": "Filter mask", + "description": "Polygon coordinates defining where this filter applies within the frame." + }, + "raw_mask": { + "label": "Raw Mask" + } } }, "record": { @@ -1597,4 +1632,4 @@ "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication." } } -} \ No newline at end of file +} diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index 3d27abb012..371a1f5140 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -1,12 +1,60 @@ -import type { FrigateConfig } from "@/types/frigateConfig"; +import type { HiddenFieldContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; import type { SectionConfigOverrides } from "./types"; // Attribute labels (face, license_plate, Frigate+ couriers like DHL/Amazon, -// etc.) are populated into objects.filters by the backend even when the -// model can't actually detect them. They aren't user-settable, so hide any -// `filters.` patterns from forms and override comparisons. -const hideAttributeFilters = (config: FrigateConfig): string[] => - (config.model?.all_attributes ?? []).map((attr) => `filters.${attr}`); +// etc.) are populated into objects.filters by the backend for every +// attribute the model knows about. +// +// - Untracked attributes: hide the whole `filters.` collapsible. +// - Tracked attributes: strip the FilterConfig fields we don't expose +// (`threshold`, `min_ratio`, `max_ratio`) from the form data so RJSF +// doesn't surface them as ad-hoc additionalProperties entries under the +// restricted AttributeFilter schema (see modifySchemaForSection objects +// branch). The data is sanitized out symmetrically from the baseline +// too, so power-user YAML values for those fields are preserved on save +// (buildOverrides only emits diffs of fields the form has seen). +const ATTRIBUTE_FILTER_HIDDEN_SUBFIELDS = [ + "threshold", + "min_ratio", + "max_ratio", +]; + +const hideAttributeFilters = ({ + fullConfig, + fullCameraConfig, + level, + formData, +}: HiddenFieldContext): string[] => { + const trackFromForm = Array.isArray( + (formData as { track?: unknown } | undefined)?.track, + ) + ? (formData as { track: string[] }).track + : undefined; + + const track = + trackFromForm ?? + (level !== "global" ? fullCameraConfig?.objects?.track : undefined) ?? + fullConfig.objects?.track ?? + []; + + const attrs = getEffectiveAttributeLabels( + fullConfig, + fullCameraConfig, + level, + ); + const hidden: string[] = []; + for (const attr of attrs) { + if (!track.includes(attr)) { + hidden.push(`filters.${attr}`); + } else { + for (const field of ATTRIBUTE_FILTER_HIDDEN_SUBFIELDS) { + hidden.push(`filters.${attr}.${field}`); + } + } + } + return hidden; +}; const objects: SectionConfigOverrides = { base: { diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 9245fa2409..e61ac8a6a7 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -308,11 +308,30 @@ export function ConfigSection({ // Get section schema using cached hook const sectionSchema = useSectionSchema(sectionPath, effectiveLevel); - // Apply special case handling for sections with problematic schema defaults + // Apply special case handling for sections with problematic schema defaults. + // The HiddenFieldContext is built from `config` (saved state) only — not the + // in-flight raw section value — because the schema is computed before + // rawFormData is derived. The objects-branch fallback in + // modifySchemaForSection reads `track` from fullCameraConfig / fullConfig. const modifiedSchema = useMemo( () => - modifySchemaForSection(sectionPath, level, sectionSchema ?? undefined), - [sectionPath, level, sectionSchema], + modifySchemaForSection( + sectionPath, + level, + sectionSchema ?? undefined, + config + ? { + fullConfig: config, + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + } + : undefined, + ), + [sectionPath, level, sectionSchema, config, effectiveLevel, cameraName], ); // Get override status (camera vs global) @@ -384,7 +403,19 @@ export function ConfigSection({ // When editing a profile, hide fields that require a restart since they // cannot take effect via profile switching alone. const effectiveHiddenFields = useMemo(() => { - const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, config); + const ctx = config + ? { + fullConfig: config, + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + formData: rawFormData, + } + : undefined; + const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, ctx); if (!profileName || !sectionConfig.restartRequired?.length) { return base; } @@ -394,6 +425,10 @@ export function ConfigSection({ sectionConfig.hiddenFields, sectionConfig.restartRequired, config, + effectiveLevel, + cameraName, + level, + rawFormData, ]); const sanitizeSectionData = useCallback( diff --git a/web/src/components/config-form/sections/CameraOverridesBadge.tsx b/web/src/components/config-form/sections/CameraOverridesBadge.tsx index 466934a770..9d3dde29d6 100644 --- a/web/src/components/config-form/sections/CameraOverridesBadge.tsx +++ b/web/src/components/config-form/sections/CameraOverridesBadge.tsx @@ -20,6 +20,7 @@ import type { ProfilesApiResponse } from "@/types/profile"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { formatList } from "@/utils/stringUtil"; import { + buildHiddenFieldContext, getEffectiveHiddenFields, pathMatchesHiddenPattern, } from "@/utils/configUtil"; @@ -187,7 +188,7 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) { const hiddenFields = getEffectiveHiddenFields( sectionPath, "global", - config, + buildHiddenFieldContext(config, "global"), ); if (hiddenFields.length === 0) return rawEntries; return rawEntries diff --git a/web/src/components/config-form/sections/section-special-cases.ts b/web/src/components/config-form/sections/section-special-cases.ts index 62a4bfa85a..256c275ebb 100644 --- a/web/src/components/config-form/sections/section-special-cases.ts +++ b/web/src/components/config-form/sections/section-special-cases.ts @@ -9,7 +9,8 @@ import { RJSFSchema } from "@rjsf/utils"; import { applySchemaDefaults } from "@/lib/config-schema"; import { isJsonObject } from "@/lib/utils"; -import { JsonObject, JsonValue } from "@/types/configForm"; +import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; /** * Sections that require special handling at the global level. @@ -37,13 +38,28 @@ export function isSpecialCaseSection( * * - detectors: Strip the "default" field to prevent RJSF from merging the * default {"cpu": {"type": "cpu"}} with stored detector keys. + * - genai: Inject a default provider value on the additionalProperties shape. + * - objects: Promote tracked attribute labels (face, license_plate, courier + * logos) from `filters.additionalProperties` to explicit + * `filters.properties.` entries with a restricted FilterConfig + * shape, so RJSF renders just that one field for + * attribute filters. Non-attribute tracked labels (person, car, …) + * keep flowing through the unmodified `additionalProperties` and render + * the full FilterConfig form. */ export function modifySchemaForSection( sectionPath: string, level: string, schema: RJSFSchema | undefined, + ctx?: HiddenFieldContext, ): RJSFSchema | undefined { - if (!schema || !isSpecialCaseSection(sectionPath, level)) { + if (!schema) return schema; + + if (sectionPath === "objects") { + return modifyObjectsSchema(schema, ctx); + } + + if (!isSpecialCaseSection(sectionPath, level)) { return schema; } @@ -79,6 +95,151 @@ export function modifySchemaForSection( return schema; } +/** + * Build a stripped FilterConfig schema for tracked attribute filters + * (face, license_plate, etc.). Keeps only the fields meaningful for + * attribute detections — `min_score`, `min_area`, `max_area`. `threshold` + * and the ratio fields aren't exposed: attributes don't flow through + * `_is_false_positive` (no median-of-history check), and aspect-ratio + * filtering isn't a typical attribute-tuning knob. + * + * `min_area` and `max_area` are `Union[int, float]` in Pydantic which + * emits as `anyOf` in JSON schema; we flatten to a plain `number` so RJSF + * doesn't render the int/float type-selector dropdown for each attribute + * filter. The backend still accepts either int (pixels) or float + * (percentage) since the underlying FilterConfig union is unchanged. + */ +function buildAttributeFilterSchema( + filterConfigSchema: RJSFSchema, + attributeLabel: string, +): RJSFSchema { + const props = isJsonObject( + (filterConfigSchema as { properties?: unknown }).properties, + ) + ? (filterConfigSchema as { properties: Record }) + .properties + : undefined; + + const minScoreSchema = + props && props.min_score ? props.min_score : { type: "number" }; + + const flattenToNumber = (src: RJSFSchema | undefined): RJSFSchema => { + if (!src) return { type: "number" }; + const { anyOf: _anyOf, ...rest } = src as { + anyOf?: unknown; + [k: string]: unknown; + }; + return { ...rest, type: "number" } as RJSFSchema; + }; + + return { + type: "object", + title: attributeLabel, + properties: { + min_score: minScoreSchema, + min_area: flattenToNumber(props && props.min_area), + max_area: flattenToNumber(props && props.max_area), + }, + additionalProperties: false, + } as RJSFSchema; +} + +function modifyObjectsSchema( + schema: RJSFSchema, + ctx: HiddenFieldContext | undefined, +): RJSFSchema { + if (!ctx) return schema; + + const allAttributes = getEffectiveAttributeLabels( + ctx.fullConfig, + ctx.fullCameraConfig, + ctx.level, + ); + + // Resolve effective track at this scope, falling back through camera + // config then global config (matches hideAttributeFilters in objects.ts). + const trackFromForm = Array.isArray( + (ctx.formData as { track?: unknown } | undefined)?.track, + ) + ? (ctx.formData as { track: string[] }).track + : undefined; + const track = + trackFromForm ?? + (ctx.level !== "global" + ? ctx.fullCameraConfig?.objects?.track + : undefined) ?? + ctx.fullConfig.objects?.track ?? + []; + + if (track.length === 0) return schema; + + const schemaProperties = isJsonObject( + (schema as { properties?: unknown }).properties, + ) + ? (schema as { properties: Record }).properties + : undefined; + const filtersSchema = + schemaProperties && schemaProperties.filters + ? schemaProperties.filters + : undefined; + if (!filtersSchema) return schema; + + const filterEntrySchema = isJsonObject( + (filtersSchema as { additionalProperties?: unknown }).additionalProperties, + ) + ? (filtersSchema as { additionalProperties: RJSFSchema }) + .additionalProperties + : undefined; + if (!filterEntrySchema) return schema; + + const attributeSet = new Set(allAttributes); + const existingProperties = isJsonObject( + (filtersSchema as { properties?: unknown }).properties, + ) + ? (filtersSchema as { properties: Record }).properties + : {}; + + // Promote every tracked label to an explicit property entry so RJSF + // renders it as a normal collapsible (no additionalProperties key/value + // editor UI). Attribute labels get a restricted shape with only + // `min_score`; non-attribute labels get the full FilterConfig. Sorted + // alphabetically so the filter collapsibles match the order of the + // sibling `track` switches. + const sortedTrackedLabels = track + .filter((label): label is string => typeof label === "string") + .slice() + .sort((a, b) => a.localeCompare(b)); + const updatedFilterProperties: Record = { + ...existingProperties, + }; + for (const label of sortedTrackedLabels) { + if (attributeSet.has(label)) { + updatedFilterProperties[label] = buildAttributeFilterSchema( + filterEntrySchema, + label, + ); + } else { + updatedFilterProperties[label] = { + ...filterEntrySchema, + title: label, + } as RJSFSchema; + } + } + + const updatedFiltersSchema: RJSFSchema = { + ...filtersSchema, + properties: updatedFilterProperties, + }; + + return { + ...schema, + properties: { + ...schemaProperties, + filters: updatedFiltersSchema, + }, + }; +} + /** * Get effective defaults for sections with special schema patterns. * diff --git a/web/src/components/config-form/theme/utils/i18n.ts b/web/src/components/config-form/theme/utils/i18n.ts index 5a27020655..a5f7ea1527 100644 --- a/web/src/components/config-form/theme/utils/i18n.ts +++ b/web/src/components/config-form/theme/utils/i18n.ts @@ -6,6 +6,7 @@ */ import type { ConfigFormContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; @@ -70,12 +71,27 @@ export function buildTranslationPath( (segment): segment is string => typeof segment === "string", ); - // Handle filters section - skip the dynamic filter object name - // Example: filters.person.threshold -> filters.threshold + // Handle filters section - skip the dynamic filter object name. Route + // to `filters_attribute.` when the dynamic key is an attribute + // label (face, license_plate, courier logos) so attribute filter fields + // pick up the attribute-worded translations emitted by + // generate_config_translations.py. + // Example: filters.person.threshold -> filters.threshold + // Example: filters.face.min_area -> filters_attribute.min_area const filtersIndex = stringSegments.indexOf("filters"); if (filtersIndex !== -1 && stringSegments.length > filtersIndex + 2) { + const filterKey = stringSegments[filtersIndex + 1]; + const allAttributes = getEffectiveAttributeLabels( + formContext?.fullConfig, + formContext?.fullCameraConfig, + formContext?.level, + ); + const sectionWord = allAttributes.includes(filterKey) + ? "filters_attribute" + : "filters"; const normalized = [ - ...stringSegments.slice(0, filtersIndex + 1), + ...stringSegments.slice(0, filtersIndex), + sectionWord, ...stringSegments.slice(filtersIndex + 2), ]; return normalized.join("."); diff --git a/web/src/hooks/use-config-override.ts b/web/src/hooks/use-config-override.ts index 90ce717293..2b0ed2cbd4 100644 --- a/web/src/hooks/use-config-override.ts +++ b/web/src/hooks/use-config-override.ts @@ -10,6 +10,7 @@ import { FrigateConfig } from "@/types/frigateConfig"; import { JsonObject, JsonValue } from "@/types/configForm"; import { isJsonObject } from "@/lib/utils"; import { + buildHiddenFieldContext, getBaseCameraSectionValue, getEffectiveHiddenFields, pathMatchesHiddenPattern, @@ -286,7 +287,7 @@ export function useConfigOverride({ const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const collapsedGlobal = stripHiddenPaths( collapseEmpty(normalizedGlobalValue), @@ -439,7 +440,11 @@ export function useAllCameraOverrides( getBaseCameraSectionValue(config, cameraName, key), ); - const hiddenFields = getEffectiveHiddenFields(key, "camera", config); + const hiddenFields = getEffectiveHiddenFields( + key, + "camera", + buildHiddenFieldContext(config, "camera", cameraName), + ); const collapsedGlobal = stripHiddenPaths( collapseEmpty(globalValue), hiddenFields, @@ -795,7 +800,7 @@ export function useCameraSectionDeltas( const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const deltas: FieldDelta[] = []; @@ -864,7 +869,7 @@ export function useProfileSectionDeltas( const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const deltas: FieldDelta[] = []; diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6ebfa92638..c83dbcc1c9 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -89,6 +89,7 @@ import { mutate } from "swr"; import { RJSFSchema } from "@rjsf/utils"; import { buildConfigDataForPath, + buildHiddenFieldContext, flattenOverrides, getSectionConfig, parseProfileFromSectionPath, @@ -851,11 +852,11 @@ export default function Settings() { // they stay in sync with what the embedded forms strip on render const detectorHiddenFields = resolveHiddenFieldEntries( getSectionConfig("detectors", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ); const modelHiddenFields = resolveHiddenFieldEntries( getSectionConfig("model", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ); const sanitizedDetectors = pendingDetectors !== undefined diff --git a/web/src/types/configForm.ts b/web/src/types/configForm.ts index 03ecd3e4d9..1f4c57c393 100644 --- a/web/src/types/configForm.ts +++ b/web/src/types/configForm.ts @@ -13,7 +13,19 @@ export type JsonArray = JsonValue[]; export type ConfigSectionData = JsonObject; -export type HiddenFieldEntry = string | ((config: FrigateConfig) => string[]); +export type HiddenFieldContext = { + fullConfig: FrigateConfig; + fullCameraConfig?: CameraConfig; + level: "global" | "camera" | "replay"; + cameraName?: string; + // Saved form data for the current section/scope (i.e. rawFormData in + // BaseSection.tsx). Not the user's in-flight RJSF edits. Optional because + // most hidden-field callsites compute patterns without a specific section + // value on hand; resolvers fall back to fullCameraConfig / fullConfig. + formData?: ConfigSectionData; +}; + +export type HiddenFieldEntry = string | ((ctx: HiddenFieldContext) => string[]); export type ConfigFormContext = { level?: "global" | "camera"; diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts index 2b9a05a1a3..68a2822200 100644 --- a/web/src/types/frigateConfig.ts +++ b/web/src/types/frigateConfig.ts @@ -522,8 +522,8 @@ export interface FrigateConfig { path: string | null; width: number; colormap: { [key: string]: [number, number, number] }; - attributes_map: { [key: string]: [string] }; - all_attributes: [string]; + attributes_map: { [key: string]: string[] }; + all_attributes: string[]; plus?: { name: string; id: string; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index 80c940cb70..4b6ffefb71 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -19,9 +19,10 @@ import { sanitizeOverridesForSection, } from "@/components/config-form/sections/section-special-cases"; import type { RJSFSchema } from "@rjsf/utils"; -import type { FrigateConfig } from "@/types/frigateConfig"; +import type { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import type { ConfigSectionData, + HiddenFieldContext, JsonObject, JsonValue, } from "@/types/configForm"; @@ -568,6 +569,17 @@ export function prepareSectionSavePayload(opts: { schemaSection, level, sectionSchema, + config + ? { + fullConfig: config, + fullCameraConfig: + level === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + } + : undefined, ); // Compute rawFormData (the current stored value for this section) @@ -615,10 +627,16 @@ export function prepareSectionSavePayload(opts: { // For profile sections, also hide restart-required fields to match // effectiveHiddenFields in BaseSection (prevents spurious deletion markers // for fields that are hidden from the form during profile editing). - const resolvedHidden = resolveHiddenFieldEntries( - sectionConfig.hiddenFields, - config, - ); + const resolvedHidden = resolveHiddenFieldEntries(sectionConfig.hiddenFields, { + fullConfig: config, + fullCameraConfig: + level === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + formData: rawFormData as ConfigSectionData, + }); const hiddenFieldsForSanitize = profileInfo.isProfile && sectionConfig.restartRequired?.length ? [...new Set([...resolvedHidden, ...sectionConfig.restartRequired])] @@ -731,32 +749,77 @@ export function getSectionConfig( return mergeSectionConfig(entry.base, overrides); } +/** + * Resolve the effective attribute label set at a given scope. At camera + * (and replay) scope on a dedicated LPR camera (`camera.type === "lpr"`), + * `license_plate` is treated as a regular tracked object — not an + * attribute — to match the backend's per-camera carve-out in + * `frigate/video/detect.py`. Returns the full attribute list at global + * scope and for non-LPR cameras. + */ +export function getEffectiveAttributeLabels( + fullConfig: FrigateConfig | undefined, + fullCameraConfig: CameraConfig | undefined, + level: "global" | "camera" | "replay" | undefined, +): string[] { + const all = fullConfig?.model?.all_attributes ?? []; + if (level !== "global" && fullCameraConfig?.type === "lpr") { + return all.filter((attr) => attr !== "license_plate"); + } + return all; +} + +/** + * Build a `HiddenFieldContext` for the common case where a callsite has + * `config`, an optional `cameraName`, and a level, but no per-section + * saved form data to thread through. Resolvers that don't read `formData` + * (which is most of them) just fall through to `fullCameraConfig` / + * `fullConfig`. + */ +export function buildHiddenFieldContext( + config: FrigateConfig | undefined, + level: "global" | "camera" | "replay", + cameraName?: string, +): HiddenFieldContext | undefined { + if (!config) return undefined; + return { + fullConfig: config, + fullCameraConfig: + level !== "global" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + }; +} + /** * Resolve the effective hidden-field patterns for a section. Each entry in * `hiddenFields` is either a literal pattern or a function that produces - * patterns from the loaded config (e.g. `filters.` for each - * `model.all_attributes` entry on the objects section). + * patterns from the loaded config and scope (e.g. `filters.` for each + * `model.all_attributes` entry on the objects section, gated by the + * effective `objects.track` list at the current scope). */ export function getEffectiveHiddenFields( sectionKey: string, level: "global" | "camera" | "replay", - config: FrigateConfig | undefined, + ctx: HiddenFieldContext | undefined, ): string[] { return resolveHiddenFieldEntries( getSectionConfig(sectionKey, level).hiddenFields, - config, + ctx, ); } export function resolveHiddenFieldEntries( entries: SectionConfig["hiddenFields"] | undefined, - config: FrigateConfig | undefined, + ctx: HiddenFieldContext | undefined, ): string[] { if (!entries || entries.length === 0) return []; const result: string[] = []; for (const entry of entries) { if (typeof entry === "function") { - if (config) result.push(...entry(config)); + if (ctx) result.push(...entry(ctx)); } else { result.push(entry); } diff --git a/web/src/views/settings/DetectorsAndModelSettingsView.tsx b/web/src/views/settings/DetectorsAndModelSettingsView.tsx index 615ab3296c..ebbad2b523 100644 --- a/web/src/views/settings/DetectorsAndModelSettingsView.tsx +++ b/web/src/views/settings/DetectorsAndModelSettingsView.tsx @@ -51,6 +51,7 @@ import { ConfigSectionTemplate } from "@/components/config-form/sections"; import { ConfigMessageBanner } from "@/components/config-form/ConfigMessageBanner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { + buildHiddenFieldContext, getSectionConfig, resolveHiddenFieldEntries, sanitizeSectionData, @@ -226,7 +227,7 @@ export default function DetectorsAndModelSettingsView({ () => resolveHiddenFieldEntries( getSectionConfig("detectors", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ), [config], ); @@ -234,7 +235,7 @@ export default function DetectorsAndModelSettingsView({ () => resolveHiddenFieldEntries( getSectionConfig("model", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ), [config], ); From b0b00fe1d0f7838c59bba0c2125064829ffc7a7f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 19 May 2026 12:03:57 -0600 Subject: [PATCH 09/94] GenAI Refactor (#23253) * Ensure runtime options are passed * Add attribute info to prompt when configured * Move GenAI plugins to dedicated directory * Migrate prompts to dedicated folder * Move chat prompts to prompts * Implement reasoning traces in the UI * Cleanup * Make azure a subclass of openai * Implement reasoning for other providers * mypy * Cleanup --- frigate/api/chat.py | 502 ++----------- frigate/api/defs/response/chat_response.py | 4 + frigate/genai/__init__.py | 185 +---- frigate/genai/azure-openai.py | 315 --------- frigate/genai/plugins/__init__.py | 1 + frigate/genai/plugins/azure-openai.py | 53 ++ frigate/genai/{ => plugins}/gemini.py | 42 +- frigate/genai/{ => plugins}/llama_cpp.py | 23 +- frigate/genai/{ => plugins}/ollama.py | 16 + frigate/genai/{ => plugins}/openai.py | 27 +- frigate/genai/prompts.py | 739 ++++++++++++++++++++ web/public/locales/en/views/chat.json | 5 + web/src/components/chat/ReasoningBubble.tsx | 87 +++ web/src/pages/Chat.tsx | 24 +- web/src/types/chat.ts | 1 + web/src/utils/chatUtil.ts | 14 + 16 files changed, 1108 insertions(+), 930 deletions(-) delete mode 100644 frigate/genai/azure-openai.py create mode 100644 frigate/genai/plugins/__init__.py create mode 100644 frigate/genai/plugins/azure-openai.py rename frigate/genai/{ => plugins}/gemini.py (93%) rename frigate/genai/{ => plugins}/llama_cpp.py (96%) rename frigate/genai/{ => plugins}/ollama.py (95%) rename frigate/genai/{ => plugins}/openai.py (93%) create mode 100644 frigate/genai/prompts.py create mode 100644 web/src/components/chat/ReasoningBubble.tsx diff --git a/frigate/api/chat.py b/frigate/api/chat.py index 8b2af8b265..291503dbba 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -35,9 +35,13 @@ from frigate.api.defs.response.chat_response import ( ToolCall, ) from frigate.api.defs.tags import Tags -from frigate.api.event import events +from frigate.api.event import _build_attribute_filter_clause, events from frigate.config import FrigateConfig -from frigate.config.ui import UnitSystemEnum +from frigate.genai.prompts import ( + build_chat_system_prompt, + get_attribute_classifications, + get_tool_definitions, +) from frigate.genai.utils import build_assistant_message_for_conversation from frigate.jobs.vlm_watch import ( get_vlm_watch_job, @@ -68,390 +72,6 @@ class VLMMonitorRequest(BaseModel): zones: List[str] = [] -def get_tool_definitions( - semantic_search_enabled: bool = False, -) -> List[Dict[str, Any]]: - """ - Get OpenAI-compatible tool definitions for Frigate. - - Returns a list of tool definitions that can be used with OpenAI-compatible - function calling APIs. When semantic search is enabled, the search_objects - tool exposes an additional `semantic_query` parameter for descriptive - queries (e.g. "person riding a lawn mower") and find_similar_objects is - included. - """ - search_objects_properties: Dict[str, Any] = { - "camera": { - "type": "string", - "description": "Camera name to filter by (optional).", - }, - "label": { - "type": "string", - "description": ( - "Generic object class to filter by — one of the tracked detector " - "labels such as 'person', 'package', 'car', 'dog', 'bird'. Use " - "this for broad queries like 'show me all cars today'. Combine " - "with semantic_query when the user also describes appearance or " - "behavior (e.g. label='person', semantic_query='riding a lawn " - "mower')." - ), - }, - "sub_label": { - "type": "string", - "description": ( - "Filter by a DISCRETE NAMED entity recognized in the detection. " - "Use this for: a known person's name ('John'), a delivery " - "company ('Amazon', 'UPS'), a recognized animal species or " - "breed ('blue jay', 'cardinal', 'golden retriever'), or a " - "license plate string. When filtering by a specific name, set " - "only sub_label and leave label unset. Do NOT use sub_label " - "for descriptions of appearance, clothing, or actions — those " - "belong in semantic_query." - ), - }, - "after": { - "type": "string", - "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", - }, - "before": { - "type": "string", - "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", - }, - "zones": { - "type": "array", - "items": {"type": "string"}, - "description": "List of zone names to filter by.", - }, - "limit": { - "type": "integer", - "description": "Maximum number of objects to return (default: 25).", - "default": 25, - }, - } - - if semantic_search_enabled: - search_objects_properties["semantic_query"] = { - "type": "string", - "description": ( - "Optional natural-language description of a PHYSICAL " - "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, " - "used to semantically narrow results. Only set this when the " - "user describes something beyond what label and sub_label can " - "express on their own.\n" - "USE for descriptive phrases like: 'riding a lawn mower', " - "'wearing a red jacket', 'carrying a package', 'walking a " - "dog', 'on a bicycle', 'holding an umbrella'.\n" - "DO NOT USE for:\n" - "- specific named people, pets, or delivery companies → use sub_label\n" - "- animal species or breed names like 'blue jay', 'cardinal', " - "'golden retriever' → use sub_label\n" - "- license plate strings → use sub_label\n" - "- generic object queries like 'all cars today' or 'every " - "person' → use label alone with no semantic_query\n" - "When set, combine with label/time/camera/zone filters as " - "usual (e.g. label='person', semantic_query='riding a lawn " - "mower', after='2024-05-01T00:00:00Z')." - ), - } - - search_objects_description = ( - "Search the historical record of detected objects in Frigate. " - "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', " - "'when was the last car?', 'show me detections from yesterday'. " - "Do NOT use this for monitoring or alerting requests about future events — " - "use start_camera_watch instead for those. " - "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n" - "Choose filters based on what the user is asking for:\n" - "- Generic class query ('show me all cars today'): set `label` only.\n" - "- Specific NAMED entity (known person, delivery company, animal " - "species/breed like 'blue jay' or 'golden retriever', license " - "plate): set `sub_label` only and leave `label` unset.\n" - ) - if semantic_search_enabled: - search_objects_description += ( - "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a " - "discrete name ('person riding a lawn mower', 'someone in a red " - "jacket', 'person carrying a package'): set `semantic_query` with " - "the descriptive phrase, optionally alongside `label` for the " - "object class. Do NOT put descriptive phrases in sub_label." - ) - - return [ - { - "type": "function", - "function": { - "name": "search_objects", - "description": search_objects_description, - "parameters": { - "type": "object", - "properties": search_objects_properties, - }, - "required": [], - }, - }, - { - "type": "function", - "function": { - "name": "find_similar_objects", - "description": ( - "Find tracked objects that are visually and semantically similar " - "to a specific past event. Use this when the user references a " - "particular object they have seen and wants to find other " - "sightings of the same or similar one ('that green car', 'the " - "person in the red jacket', 'the package that was delivered'). " - "Prefer this over search_objects whenever the user's intent is " - "'find more like this specific one.' Use search_objects first " - "only if you need to locate the anchor event. Requires semantic " - "search to be enabled." - ), - "parameters": { - "type": "object", - "properties": { - "event_id": { - "type": "string", - "description": "The id of the anchor event to find similar objects to.", - }, - "after": { - "type": "string", - "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", - }, - "before": { - "type": "string", - "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", - }, - "cameras": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of cameras to restrict to. Defaults to all.", - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.", - }, - "sub_labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of sub_labels (names) to restrict to.", - }, - "zones": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of zones. An event matches if any of its zones overlap.", - }, - "similarity_mode": { - "type": "string", - "enum": ["visual", "semantic", "fused"], - "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.", - "default": "fused", - }, - "min_score": { - "type": "number", - "description": "Drop matches with a similarity score below this threshold (0.0-1.0).", - }, - "limit": { - "type": "integer", - "description": "Maximum number of matches to return (default: 10).", - "default": 10, - }, - }, - "required": ["event_id"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "set_camera_state", - "description": ( - "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). " - "Use camera='*' to apply to all cameras at once. " - "Only call this tool when the user explicitly asks to change a camera setting. " - "Requires admin privileges." - ), - "parameters": { - "type": "object", - "properties": { - "camera": { - "type": "string", - "description": "Camera name to target, or '*' to target all cameras.", - }, - "feature": { - "type": "string", - "enum": [ - "detect", - "record", - "snapshots", - "audio", - "motion", - "enabled", - "birdseye", - "birdseye_mode", - "improve_contrast", - "ptz_autotracker", - "motion_contour_area", - "motion_threshold", - "notifications", - "audio_transcription", - "review_alerts", - "review_detections", - "object_descriptions", - "review_descriptions", - "profile", - ], - "description": ( - "The feature to change. Most features accept ON or OFF. " - "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. " - "motion_contour_area and motion_threshold accept a number. " - "profile accepts a profile name or 'none' to deactivate (requires camera='*')." - ), - }, - "value": { - "type": "string", - "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.", - }, - }, - "required": ["camera", "feature", "value"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "get_live_context", - "description": ( - "Get the current live image and detection information for a camera: objects being tracked, " - "zones, timestamps. Use this to understand what is visible in the live view. " - "Call this when answering questions about what is happening right now on a specific camera." - ), - "parameters": { - "type": "object", - "properties": { - "camera": { - "type": "string", - "description": "Camera name to get live context for.", - }, - }, - "required": ["camera"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "start_camera_watch", - "description": ( - "Start a continuous VLM watch job that monitors a camera and sends a notification " - "when a specified condition is met. Use this when the user wants to be alerted about " - "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. " - "Only one watch job can run at a time. Returns a job ID." - ), - "parameters": { - "type": "object", - "properties": { - "camera": { - "type": "string", - "description": "Camera ID to monitor.", - }, - "condition": { - "type": "string", - "description": ( - "Natural-language description of the condition to watch for, " - "e.g. 'a person arrives at the front door'." - ), - }, - "max_duration_minutes": { - "type": "integer", - "description": "Maximum time to watch before giving up (minutes, default 60).", - "default": 60, - }, - "labels": { - "type": "array", - "items": {"type": "string"}, - "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.", - }, - "zones": { - "type": "array", - "items": {"type": "string"}, - "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.", - }, - }, - "required": ["camera", "condition"], - }, - }, - }, - { - "type": "function", - "function": { - "name": "stop_camera_watch", - "description": ( - "Cancel the currently running VLM watch job. Use this when the user wants to " - "stop a previously started watch, e.g. 'stop watching the front door'." - ), - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - }, - { - "type": "function", - "function": { - "name": "get_profile_status", - "description": ( - "Get the current profile status including the active profile and " - "timestamps of when each profile was last activated. Use this to " - "determine time periods for recap requests — e.g. when the user asks " - "'what happened while I was away?', call this first to find the relevant " - "time window based on profile activation history." - ), - "parameters": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - }, - { - "type": "function", - "function": { - "name": "get_recap", - "description": ( - "Get a recap of all activity (alerts and detections) for a given time period. " - "Use this after calling get_profile_status to retrieve what happened during " - "a specific window — e.g. 'what happened while I was away?'. Returns a " - "chronological list of activity with camera, objects, zones, and GenAI-generated " - "descriptions when available. Summarize the results for the user." - ), - "parameters": { - "type": "object", - "properties": { - "after": { - "type": "string", - "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').", - }, - "before": { - "type": "string", - "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').", - }, - "cameras": { - "type": "string", - "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.", - }, - "severity": { - "type": "string", - "enum": ["alert", "detection"], - "description": "Filter by severity level. Omit to include both alerts and detections.", - }, - }, - "required": ["after", "before"], - }, - }, - }, - ] - - @router.get( "/chat/tools", dependencies=[Depends(allow_any_authenticated())], @@ -460,10 +80,13 @@ def get_tool_definitions( ) def get_tools(request: Request) -> JSONResponse: """Get list of available tools for LLM function calling.""" - semantic_search_enabled = bool( - getattr(request.app.frigate_config.semantic_search, "enabled", False) + config = request.app.frigate_config + semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False)) + attribute_classifications = get_attribute_classifications(config) + tools = get_tool_definitions( + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, ) - tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled) return JSONResponse(content={"tools": tools}) @@ -554,11 +177,14 @@ async def _execute_search_objects( elif zones is None: zones = "all" + attribute = arguments.get("attribute") + # Build query parameters compatible with EventsQueryParams query_params = EventsQueryParams( cameras=arguments.get("camera", "all"), labels=arguments.get("label", "all"), sub_labels=arguments.get("sub_label", "all"), # case-insensitive on the backend + attributes=attribute if attribute else "all", zones=zones, zone=zones, after=after, @@ -626,6 +252,7 @@ async def _execute_search_objects_semantic( label = arguments.get("label") sub_label = arguments.get("sub_label") + attribute = arguments.get("attribute") zones = arguments.get("zones") if isinstance(zones, list) and zones: @@ -668,6 +295,10 @@ async def _execute_search_objects_semantic( if sub_label: # case-insensitive match to mirror events() behavior clauses.append(fn.LOWER(Event.sub_label.cast("text")) == sub_label.lower()) + if attribute: + attribute_clause = _build_attribute_filter_clause(attribute) + if attribute_clause is not None: + clauses.append(attribute_clause) if zones: zone_clauses = [Event.zones.cast("text") % f'*"{zone}"*' for zone in zones] clauses.append(reduce(operator.or_, zone_clauses)) @@ -1481,72 +1112,19 @@ async def chat_completion( config = request.app.frigate_config semantic_search_enabled = bool(getattr(config.semantic_search, "enabled", False)) - tools = get_tool_definitions(semantic_search_enabled=semantic_search_enabled) + attribute_classifications = get_attribute_classifications(config) + tools = get_tool_definitions( + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) conversation = [] - current_datetime = datetime.now() - current_date_str = current_datetime.strftime("%Y-%m-%d") - current_time_str = current_datetime.strftime("%I:%M:%S %p") - - cameras_info = [] - has_speed_zone = False - for camera_id in allowed_cameras: - if camera_id not in config.cameras: - continue - camera_config = config.cameras[camera_id] - friendly_name = ( - camera_config.friendly_name - if camera_config.friendly_name - else camera_id.replace("_", " ").title() - ) - zone_names = list(camera_config.zones.keys()) - if not has_speed_zone: - has_speed_zone = any( - zone.distances for zone in camera_config.zones.values() - ) - if zone_names: - cameras_info.append( - f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" - ) - else: - cameras_info.append(f" - {friendly_name} (ID: {camera_id})") - - cameras_section = "" - if cameras_info: - cameras_section = ( - "\n\nAvailable cameras:\n" - + "\n".join(cameras_info) - + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." - ) - - speed_units_section = "" - if has_speed_zone: - speed_unit = ( - "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h" - ) - speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}." - - semantic_search_section = "" - if semantic_search_enabled: - semantic_search_section = ( - "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n" - "- Generic class ('show me all cars today'): set `label` only.\n" - "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n" - "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`." - ) - - system_prompt = f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events. - -Current server local date and time: {current_date_str} at {current_time_str} - -Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly. - -Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields. -When users ask about "today", "yesterday", "this week", etc., use the current date above as reference. -When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today). -Always be accurate with time calculations based on the current date provided. - -When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{cameras_section}{speed_units_section}""" + system_prompt = build_chat_system_prompt( + config=config, + allowed_cameras=allowed_cameras, + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ) conversation.append( { @@ -1607,6 +1185,13 @@ When a user refers to a specific object they have seen or describe with identify ) + b"\n" ) + elif kind == "reasoning_delta": + yield ( + json.dumps({"type": "reasoning", "delta": value}).encode( + "utf-8" + ) + + b"\n" + ) elif kind == "stats": yield ( json.dumps({"type": "stats", **value}).encode("utf-8") @@ -1707,6 +1292,7 @@ When a user refers to a specific object they have seen or describe with identify final_content = response.get("content") or "" if body.stream: + final_reasoning = response.get("reasoning") async def stream_body() -> Any: if tool_calls: @@ -1721,6 +1307,15 @@ When a user refers to a specific object they have seen or describe with identify ).encode("utf-8") + b"\n" ) + # Emit the full reasoning trace up front when the + # underlying client did not stream it + if final_reasoning: + yield ( + json.dumps( + {"type": "reasoning", "delta": final_reasoning} + ).encode("utf-8") + + b"\n" + ) # Stream content in word-sized chunks for smooth UX for part in chunk_content(final_content): yield ( @@ -1741,6 +1336,7 @@ When a user refers to a specific object they have seen or describe with identify message=ChatMessageResponse( role="assistant", content=final_content, + reasoning=response.get("reasoning"), tool_calls=None, ), finish_reason=response.get("finish_reason", "stop"), diff --git a/frigate/api/defs/response/chat_response.py b/frigate/api/defs/response/chat_response.py index 0bc864ba68..c2b3e6b1f2 100644 --- a/frigate/api/defs/response/chat_response.py +++ b/frigate/api/defs/response/chat_response.py @@ -20,6 +20,10 @@ class ChatMessageResponse(BaseModel): content: Optional[str] = Field( default=None, description="Message content (None if tool calls present)" ) + reasoning: Optional[str] = Field( + default=None, + description="Separated reasoning/thinking trace if the model emitted one", + ) tool_calls: Optional[list[ToolCallInvocation]] = Field( default=None, description="Tool calls if LLM wants to call tools" ) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index ce0034670d..864092df58 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -1,6 +1,5 @@ """Generative AI module for Frigate.""" -import datetime import importlib import json import logging @@ -9,13 +8,18 @@ import re from typing import Any, Callable, Optional import numpy as np -from playhouse.shortcuts import model_to_dict from pydantic import ValidationError from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum from frigate.const import CLIPS_DIR from frigate.data_processing.post.types import ReviewMetadata from frigate.genai.manager import GenAIClientManager +from frigate.genai.prompts import ( + build_object_description_prompt, + build_review_description_prompt, + build_review_description_response_format, + build_review_summary_prompt, +) from frigate.models import Event logger = logging.getLogger(__name__) @@ -61,75 +65,14 @@ class GenAIClient: activity_context_prompt: str, ) -> ReviewMetadata | None: """Generate a description for the review item activity.""" + context_prompt = build_review_description_prompt( + review_data, + thumbnails, + concerns, + preferred_language, + activity_context_prompt, + ) - def get_concern_prompt() -> str: - if concerns: - concern_list = "\n - ".join(concerns) - return f"""- `other_concerns` (list of strings): Include a list of any of the following concerns that are occurring: - - {concern_list}""" - else: - return "" - - def get_language_prompt() -> str: - if preferred_language: - return f"Provide your answer in {preferred_language}" - else: - return "" - - def get_objects_list() -> str: - if review_data["unified_objects"]: - return "\n- " + "\n- ".join(review_data["unified_objects"]) - else: - return "\n- (No objects detected)" - - context_prompt = f""" -Your task is to analyze a sequence of images taken in chronological order from a security camera. - -## Normal Activity Patterns for This Property - -{activity_context_prompt} - -## Task Instructions - -Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently. - -## Analysis Guidelines - -When forming your description: -- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list. -- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence. -- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity). -- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects. -- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved. -- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour. -- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible. -- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases. - -## Response Field Guidelines - -Respond with a JSON object matching the provided schema. Field-specific guidance: -- `observations`: Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence. -- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign. -- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles. -- `shortSummary`: Briefly summarize the primary activity across the observations. -- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above. - -## Sequence Details - -- Camera: {review_data["camera"]} -- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest) -- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds -- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"} - -## Objects in Scene - -Each line represents a detection state, not necessarily unique individuals. The `←` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times. - -**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.** -{get_objects_list()} - -{get_language_prompt()} -""" logger.debug( f"Sending {len(thumbnails)} images to create review description on {review_data['camera']}" ) @@ -143,25 +86,7 @@ Each line represents a detection state, not necessarily unique individuals. The ) as f: f.write(context_prompt) - # Build JSON schema for structured output from ReviewMetadata model - schema = ReviewMetadata.model_json_schema() - schema.get("properties", {}).pop("time", None) - - if "time" in schema.get("required", []): - schema["required"].remove("time") - if not concerns: - schema.get("properties", {}).pop("other_concerns", None) - if "other_concerns" in schema.get("required", []): - schema["required"].remove("other_concerns") - - response_format = { - "type": "json_schema", - "json_schema": { - "name": "review_metadata", - "strict": True, - "schema": schema, - }, - } + response_format = build_review_description_response_format(concerns) response = self._send(context_prompt, thumbnails, response_format) @@ -240,61 +165,9 @@ Each line represents a detection state, not necessarily unique individuals. The debug_save: bool, ) -> str | None: """Generate a summary of review item descriptions over a period of time.""" - time_range = f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')} to {datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}" - timeline_summary_prompt = f""" -You are a security officer writing a concise security report. - -Time range: {time_range} - -Input format: Each event is a JSON object with: -- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time" -- "context": array of related events from other cameras that occurred during overlapping time periods - -**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.** - -Report Structure - Use this EXACT format: - -# Security Summary - {time_range} - -## Overview -[Write 1-2 sentences summarizing the overall activity pattern during this period.] - ---- - -## Timeline - -[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.] - -### [Time Block Name] - -**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator] -- [Event title]: [Clear description incorporating contextual information from the "context" array] -- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"] -- Assessment: [Brief assessment incorporating context - if context explains the event, note it here] - -[Repeat for each event in chronological order within the time block] - ---- - -## Summary -[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."] - -Guidelines: -- List ALL events in chronological order, grouped by time blocks -- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern -- Integrate contextual information naturally - use the "context" array to enrich each event's description -- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person") -- Be concise but informative - focus on what happened and what it means -- If contextual information makes an event clearly normal, reflect that in your assessment -- Only create time blocks that have events - don't create empty sections -""" - - timeline_summary_prompt += "\n\nEvents:\n" - for event in events: - timeline_summary_prompt += f"\n{event}\n" - - if preferred_language: - timeline_summary_prompt += f"\nProvide your answer in {preferred_language}" + timeline_summary_prompt = build_review_summary_prompt( + start_ts, end_ts, events, preferred_language + ) if debug_save: with open( @@ -326,10 +199,7 @@ Guidelines: ) -> Optional[str]: """Generate a description for the frame.""" try: - prompt = camera_config.objects.genai.object_prompts.get( - str(event.label), - camera_config.objects.genai.prompt, - ).format(**model_to_dict(event)) + prompt = build_object_description_prompt(camera_config, event) except KeyError as e: logger.error(f"Invalid key in GenAI prompt: {e}") return None @@ -430,6 +300,10 @@ Guidelines: Returns: Dictionary with: - 'content': Optional[str] - The text response from the LLM, None if tool calls + - 'reasoning': Optional[str] - The separated reasoning/thinking trace + if the model emitted one (e.g. via OpenAI-compatible + `reasoning_content`). None when the model does not surface a + trace or the provider does not parse it. - 'tool_calls': Optional[List[Dict]] - List of tool calls if LLM wants to call tools. Each tool call dict has: - 'id': str - Unique identifier for this tool call @@ -441,6 +315,14 @@ Guidelines: - 'length': Hit token limit - 'error': An error occurred + Streaming counterpart `chat_with_tools_stream` yields + ``(kind, value)`` tuples where ``kind`` is one of: + - 'content_delta': value is a string fragment of the answer + - 'reasoning_delta': value is a string fragment of the reasoning + trace (emitted before content for thinking models) + - 'stats': value is a usage stats dict + - 'message': value is the final dict shape described above + Raises: NotImplementedError: If the provider doesn't implement this method. """ @@ -451,14 +333,15 @@ Guidelines: ) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } def load_providers() -> None: - package_dir = os.path.dirname(__file__) - for filename in os.listdir(package_dir): + plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") + for filename in os.listdir(plugins_dir): if filename.endswith(".py") and filename != "__init__.py": - module_name = f"frigate.genai.{filename[:-3]}" + module_name = f"frigate.genai.plugins.{filename[:-3]}" importlib.import_module(module_name) diff --git a/frigate/genai/azure-openai.py b/frigate/genai/azure-openai.py deleted file mode 100644 index 04a2b8d556..0000000000 --- a/frigate/genai/azure-openai.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Azure OpenAI Provider for Frigate AI.""" - -import base64 -import json -import logging -from typing import Any, AsyncGenerator, Optional -from urllib.parse import parse_qs, urlparse - -from openai import AzureOpenAI - -from frigate.config import GenAIProviderEnum -from frigate.genai import GenAIClient, register_genai_provider -from frigate.genai.openai import _stats_from_openai_usage - -logger = logging.getLogger(__name__) - - -@register_genai_provider(GenAIProviderEnum.azure_openai) -class OpenAIClient(GenAIClient): - """Generative AI client for Frigate using Azure OpenAI.""" - - provider: AzureOpenAI - - def _init_provider(self) -> AzureOpenAI | None: - """Initialize the client.""" - try: - parsed_url = urlparse(self.genai_config.base_url or "") - query_params = parse_qs(parsed_url.query) - api_version = query_params.get("api-version", [None])[0] - azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/" - - if not api_version: - logger.warning("Azure OpenAI url is missing API version.") - return None - - except Exception as e: - logger.warning("Error parsing Azure OpenAI url: %s", str(e)) - return None - - return AzureOpenAI( - api_key=self.genai_config.api_key, - api_version=api_version, - azure_endpoint=azure_endpoint, - ) - - def _send( - self, - prompt: str, - images: list[bytes], - response_format: Optional[dict] = None, - ) -> Optional[str]: - """Submit a request to Azure OpenAI.""" - encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] - try: - request_params = { - "model": self.genai_config.model, - "messages": [ - { - "role": "user", - "content": [{"type": "text", "text": prompt}] - + [ - { - "type": "image_url", - "image_url": { - "url": f"data:image/jpeg;base64,{image}", - "detail": "low", - }, - } - for image in encoded_images - ], - }, - ], - "timeout": self.timeout, - **self.genai_config.runtime_options, - } - if response_format: - request_params["response_format"] = response_format - result = self.provider.chat.completions.create(**request_params) - except Exception as e: - logger.warning("Azure OpenAI returned an error: %s", str(e)) - return None - if len(result.choices) > 0: - return str(result.choices[0].message.content.strip()) - return None - - def list_models(self) -> list[str]: - """Return available model IDs from Azure OpenAI.""" - try: - return sorted(m.id for m in self.provider.models.list().data) - except Exception as e: - logger.warning("Failed to list Azure OpenAI models: %s", e) - return [] - - def get_context_size(self) -> int: - """Get the context window size for Azure OpenAI.""" - return 128000 - - def chat_with_tools( - self, - messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - ) -> dict[str, Any]: - try: - openai_tool_choice = None - if tool_choice: - if tool_choice == "none": - openai_tool_choice = "none" - elif tool_choice == "auto": - openai_tool_choice = "auto" - elif tool_choice == "required": - openai_tool_choice = "required" - - request_params = { - "model": self.genai_config.model, - "messages": messages, - "timeout": self.timeout, - } - - if tools: - request_params["tools"] = tools - if openai_tool_choice is not None: - request_params["tool_choice"] = openai_tool_choice - - result = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload] - - if ( - result is None - or not hasattr(result, "choices") - or len(result.choices) == 0 - ): - return { - "content": None, - "tool_calls": None, - "finish_reason": "error", - } - - choice = result.choices[0] - message = choice.message - - content = message.content.strip() if message.content else None - - tool_calls = None - if message.tool_calls: - tool_calls = [] - for tool_call in message.tool_calls: - try: - arguments = json.loads(tool_call.function.arguments) - except (json.JSONDecodeError, AttributeError) as e: - logger.warning( - f"Failed to parse tool call arguments: {e}, " - f"tool: {tool_call.function.name if hasattr(tool_call.function, 'name') else 'unknown'}" - ) - arguments = {} - - tool_calls.append( - { - "id": tool_call.id if hasattr(tool_call, "id") else "", - "name": tool_call.function.name - if hasattr(tool_call.function, "name") - else "", - "arguments": arguments, - } - ) - - finish_reason = "error" - if hasattr(choice, "finish_reason") and choice.finish_reason: - finish_reason = choice.finish_reason - elif tool_calls: - finish_reason = "tool_calls" - elif content: - finish_reason = "stop" - - return { - "content": content, - "tool_calls": tool_calls, - "finish_reason": finish_reason, - } - - except Exception as e: - logger.warning("Azure OpenAI returned an error: %s", str(e)) - return { - "content": None, - "tool_calls": None, - "finish_reason": "error", - } - - async def chat_with_tools_stream( - self, - messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - ) -> AsyncGenerator[tuple[str, Any], None]: - """ - Stream chat with tools; yields content deltas then final message. - - Implements streaming function calling/tool usage for Azure OpenAI models. - """ - try: - openai_tool_choice = None - if tool_choice: - if tool_choice == "none": - openai_tool_choice = "none" - elif tool_choice == "auto": - openai_tool_choice = "auto" - elif tool_choice == "required": - openai_tool_choice = "required" - - request_params = { - "model": self.genai_config.model, - "messages": messages, - "timeout": self.timeout, - "stream": True, - "stream_options": {"include_usage": True}, - } - - if tools: - request_params["tools"] = tools - if openai_tool_choice is not None: - request_params["tool_choice"] = openai_tool_choice - - # Use streaming API - content_parts: list[str] = [] - tool_calls_by_index: dict[int, dict[str, Any]] = {} - finish_reason = "stop" - usage_stats: Optional[dict[str, Any]] = None - - stream = self.provider.chat.completions.create(**request_params) # type: ignore[call-overload] - - for chunk in stream: - chunk_usage = getattr(chunk, "usage", None) - if chunk_usage is not None: - usage_stats = _stats_from_openai_usage(chunk_usage) - - if not chunk or not chunk.choices: - continue - - choice = chunk.choices[0] - delta = choice.delta - - # Check for finish reason - if choice.finish_reason: - finish_reason = choice.finish_reason - - # Extract content deltas - if delta.content: - content_parts.append(delta.content) - yield ("content_delta", delta.content) - - # Extract tool calls - if delta.tool_calls: - for tc in delta.tool_calls: - idx = tc.index - fn = tc.function - - if idx not in tool_calls_by_index: - tool_calls_by_index[idx] = { - "id": tc.id or "", - "name": fn.name if fn and fn.name else "", - "arguments": "", - } - - t = tool_calls_by_index[idx] - if tc.id: - t["id"] = tc.id - if fn and fn.name: - t["name"] = fn.name - if fn and fn.arguments: - t["arguments"] += fn.arguments - - # Build final message - full_content = "".join(content_parts).strip() or None - - # Convert tool calls to list format - tool_calls_list = None - if tool_calls_by_index: - tool_calls_list = [] - for tc in tool_calls_by_index.values(): - try: - # Parse accumulated arguments as JSON - parsed_args = json.loads(tc["arguments"]) - except (json.JSONDecodeError, Exception): - parsed_args = tc["arguments"] - - tool_calls_list.append( - { - "id": tc["id"], - "name": tc["name"], - "arguments": parsed_args, - } - ) - finish_reason = "tool_calls" - - if usage_stats is not None: - yield ("stats", usage_stats) - - yield ( - "message", - { - "content": full_content, - "tool_calls": tool_calls_list, - "finish_reason": finish_reason, - }, - ) - - except Exception as e: - logger.warning("Azure OpenAI streaming returned an error: %s", str(e)) - yield ( - "message", - { - "content": None, - "tool_calls": None, - "finish_reason": "error", - }, - ) diff --git a/frigate/genai/plugins/__init__.py b/frigate/genai/plugins/__init__.py new file mode 100644 index 0000000000..e6d66077d3 --- /dev/null +++ b/frigate/genai/plugins/__init__.py @@ -0,0 +1 @@ +"""GenAI provider plugins.""" diff --git a/frigate/genai/plugins/azure-openai.py b/frigate/genai/plugins/azure-openai.py new file mode 100644 index 0000000000..3599eb0dbd --- /dev/null +++ b/frigate/genai/plugins/azure-openai.py @@ -0,0 +1,53 @@ +"""Azure OpenAI Provider for Frigate AI. + +Azure OpenAI exposes the same chat completions API as OpenAI once the +client is constructed, so this provider inherits all transport, streaming, +reasoning, and tool-calling logic from :class:`OpenAIClient` and only +overrides what is genuinely Azure-specific: + +- Client construction: parses ``api-version`` out of the configured + ``base_url`` query string and instantiates :class:`openai.AzureOpenAI` + with ``azure_endpoint`` instead of ``base_url``. Raises if the URL is + malformed; :class:`GenAIClientManager` catches the exception and + disables the provider. +- Context size: Azure does not expose a per-model ``max_model_len`` field + reliably, so we keep the historical 128K default rather than the + model-name heuristic used by OpenAI. +""" + +import logging +from urllib.parse import parse_qs, urlparse + +from openai import AzureOpenAI + +from frigate.config import GenAIProviderEnum +from frigate.genai import register_genai_provider +from frigate.genai.plugins.openai import OpenAIClient + +logger = logging.getLogger(__name__) + + +@register_genai_provider(GenAIProviderEnum.azure_openai) +class AzureOpenAIClient(OpenAIClient): + """Generative AI client for Frigate using Azure OpenAI.""" + + def _init_provider(self) -> AzureOpenAI: + """Initialize the AzureOpenAI client from the configured base_url.""" + parsed_url = urlparse(self.genai_config.base_url or "") + query_params = parse_qs(parsed_url.query) + api_version = query_params.get("api-version", [None])[0] + + if not api_version: + raise ValueError("Azure OpenAI base_url is missing api-version.") + + azure_endpoint = f"{parsed_url.scheme}://{parsed_url.netloc}/" + + return AzureOpenAI( + api_key=self.genai_config.api_key, + api_version=api_version, + azure_endpoint=azure_endpoint, + ) + + def get_context_size(self) -> int: + """Azure does not reliably surface per-model context size; use 128K.""" + return 128000 diff --git a/frigate/genai/gemini.py b/frigate/genai/plugins/gemini.py similarity index 93% rename from frigate/genai/gemini.py rename to frigate/genai/plugins/gemini.py index c1046428e6..bcac09d0e3 100644 --- a/frigate/genai/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -248,6 +248,13 @@ class GeminiClient(GenAIClient): if tool_config: config_params["tool_config"] = tool_config + # Ask thinking-capable models (Gemini 2.5+) to include their + # reasoning trace as separate `thought` parts so we can surface + # it on the reasoning channel. Older models ignore this field. + config_params["thinking_config"] = types.ThinkingConfig( + include_thoughts=True + ) + # Merge runtime_options if isinstance(self.genai_config.runtime_options, dict): config_params.update(self.genai_config.runtime_options) @@ -262,19 +269,24 @@ class GeminiClient(GenAIClient): if not response or not response.candidates: return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } candidate = response.candidates[0] content = None + reasoning_parts: list[str] = [] tool_calls = None - # Extract content and tool calls from response + # Extract content, reasoning, and tool calls from response if candidate.content and candidate.content.parts: for part in candidate.content.parts: if part.text: - content = part.text.strip() + if getattr(part, "thought", False): + reasoning_parts.append(part.text) + else: + content = part.text.strip() elif part.function_call: # Handle function call if tool_calls is None: @@ -297,6 +309,8 @@ class GeminiClient(GenAIClient): } ) + reasoning = "".join(reasoning_parts).strip() or None + # Determine finish reason finish_reason = "error" if hasattr(candidate, "finish_reason") and candidate.finish_reason: @@ -322,6 +336,7 @@ class GeminiClient(GenAIClient): return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -330,6 +345,7 @@ class GeminiClient(GenAIClient): logger.warning("Gemini API error during chat_with_tools: %s", str(e)) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -339,6 +355,7 @@ class GeminiClient(GenAIClient): ) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -477,12 +494,19 @@ class GeminiClient(GenAIClient): if tool_config: config_params["tool_config"] = tool_config + # Ask thinking-capable models to include their reasoning trace + # as separate `thought` parts (Gemini 2.5+; ignored elsewhere). + config_params["thinking_config"] = types.ThinkingConfig( + include_thoughts=True + ) + # Merge runtime_options if isinstance(self.genai_config.runtime_options, dict): config_params.update(self.genai_config.runtime_options) # Use streaming API content_parts: list[str] = [] + reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" usage_stats: Optional[dict[str, Any]] = None @@ -519,12 +543,16 @@ class GeminiClient(GenAIClient): ]: finish_reason = "error" - # Extract content and tool calls from chunk + # Extract content, reasoning, and tool calls from chunk if candidate.content and candidate.content.parts: for part in candidate.content.parts: if part.text: - content_parts.append(part.text) - yield ("content_delta", part.text) + if getattr(part, "thought", False): + reasoning_parts.append(part.text) + yield ("reasoning_delta", part.text) + else: + content_parts.append(part.text) + yield ("content_delta", part.text) elif part.function_call: # Handle function call try: @@ -565,6 +593,7 @@ class GeminiClient(GenAIClient): # Build final message full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None # Convert tool calls to list format tool_calls_list = None @@ -593,6 +622,7 @@ class GeminiClient(GenAIClient): "message", { "content": full_content, + "reasoning": full_reasoning, "tool_calls": tool_calls_list, "finish_reason": finish_reason, }, @@ -604,6 +634,7 @@ class GeminiClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, @@ -616,6 +647,7 @@ class GeminiClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, diff --git a/frigate/genai/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py similarity index 96% rename from frigate/genai/llama_cpp.py rename to frigate/genai/plugins/llama_cpp.py index 86db201288..830dd6817b 100644 --- a/frigate/genai/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -527,19 +527,28 @@ class LlamaCppClient(GenAIClient): k: v for k, v in self.provider_options.items() if k != "context_size" } payload.update(provider_opts) + payload.update(self.genai_config.runtime_options) return payload def _message_from_choice(self, choice: dict[str, Any]) -> dict[str, Any]: - """Parse OpenAI-style choice into {content, tool_calls, finish_reason}.""" + """Parse OpenAI-style choice into {content, reasoning, tool_calls, finish_reason}. + + llama.cpp's `--reasoning-format` puts the trace in + `message.reasoning_content` (preferred) or `message.thinking`; both + keys are accepted so different builds work without configuration. + """ message = choice.get("message", {}) content = message.get("content") content = content.strip() if content else None + reasoning = message.get("reasoning_content") or message.get("thinking") + reasoning = reasoning.strip() if reasoning else None tool_calls = parse_tool_calls_from_message(message) finish_reason = choice.get("finish_reason") or ( "tool_calls" if tool_calls else "stop" if content else "error" ) return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -802,6 +811,7 @@ class LlamaCppClient(GenAIClient): try: payload = self._build_payload(messages, tools, tool_choice, stream=True) content_parts: list[str] = [] + reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" @@ -831,6 +841,15 @@ class LlamaCppClient(GenAIClient): delta = choices[0].get("delta", {}) if choices[0].get("finish_reason"): finish_reason = choices[0]["finish_reason"] + # llama.cpp emits separated thinking under + # reasoning_content (preferred) or thinking before any + # content tokens arrive + reasoning_delta = delta.get("reasoning_content") or delta.get( + "thinking" + ) + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) if delta.get("content"): content_parts.append(delta["content"]) yield ("content_delta", delta["content"]) @@ -856,6 +875,7 @@ class LlamaCppClient(GenAIClient): ) full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None tool_calls_list = self._streamed_tool_calls_to_list(tool_calls_by_index) if tool_calls_list: finish_reason = "tool_calls" @@ -863,6 +883,7 @@ class LlamaCppClient(GenAIClient): "message", { "content": full_content, + "reasoning": full_reasoning, "tool_calls": tool_calls_list, "finish_reason": finish_reason, }, diff --git a/frigate/genai/ollama.py b/frigate/genai/plugins/ollama.py similarity index 95% rename from frigate/genai/ollama.py rename to frigate/genai/plugins/ollama.py index fe286f64de..a6f6d8ddd5 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/plugins/ollama.py @@ -309,6 +309,7 @@ class OllamaClient(GenAIClient): "model": self.genai_config.model, "messages": request_messages, **self.provider_options, + **self.genai_config.runtime_options, } if stream: request_params["stream"] = True @@ -336,6 +337,9 @@ class OllamaClient(GenAIClient): response.get("done"), ) content = message.get("content", "").strip() if message.get("content") else None + reasoning = ( + message.get("thinking", "").strip() if message.get("thinking") else None + ) tool_calls = parse_tool_calls_from_message(message) finish_reason = "error" if response.get("done"): @@ -348,6 +352,7 @@ class OllamaClient(GenAIClient): finish_reason = "stop" return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -431,6 +436,9 @@ class OllamaClient(GenAIClient): ) response = await async_client.chat(**request_params) result = self._message_from_response(response) + reasoning = result.get("reasoning") + if reasoning: + yield ("reasoning_delta", reasoning) content = result.get("content") if content: yield ("content_delta", content) @@ -449,6 +457,7 @@ class OllamaClient(GenAIClient): headers=self._auth_headers(), ) content_parts: list[str] = [] + reasoning_parts: list[str] = [] final_message: dict[str, Any] | None = None final_chunk: Any = None stream = await async_client.chat(**request_params) @@ -456,6 +465,10 @@ class OllamaClient(GenAIClient): if not chunk or "message" not in chunk: continue msg = chunk.get("message", {}) + reasoning_delta = msg.get("thinking") or "" + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) delta = msg.get("content") or "" if delta: content_parts.append(delta) @@ -463,8 +476,10 @@ class OllamaClient(GenAIClient): if chunk.get("done"): final_chunk = chunk full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None final_message = { "content": full_content, + "reasoning": full_reasoning, "tool_calls": None, "finish_reason": "stop", } @@ -481,6 +496,7 @@ class OllamaClient(GenAIClient): "message", { "content": "".join(content_parts).strip() or None, + "reasoning": "".join(reasoning_parts).strip() or None, "tool_calls": None, "finish_reason": "stop", }, diff --git a/frigate/genai/openai.py b/frigate/genai/plugins/openai.py similarity index 93% rename from frigate/genai/openai.py rename to frigate/genai/plugins/openai.py index f9e818fba3..f07e83b5dc 100644 --- a/frigate/genai/openai.py +++ b/frigate/genai/plugins/openai.py @@ -38,7 +38,11 @@ class OpenAIClient(GenAIClient): context_size: Optional[int] = None def _init_provider(self) -> OpenAI: - """Initialize the client.""" + """Initialize the client. + + Subclasses (e.g. Azure) should raise on configuration errors; the + manager catches construction failures and disables the provider. + """ # Extract context_size from provider_options as it's not a valid OpenAI client parameter # It will be used in get_context_size() instead provider_opts = { @@ -236,6 +240,10 @@ class OpenAIClient(GenAIClient): choice = result.choices[0] message = choice.message content = message.content.strip() if message.content else None + raw_reasoning = getattr(message, "reasoning_content", None) or getattr( + message, "reasoning", None + ) + reasoning = raw_reasoning.strip() if raw_reasoning else None tool_calls = None if message.tool_calls: @@ -270,6 +278,7 @@ class OpenAIClient(GenAIClient): return { "content": content, + "reasoning": reasoning, "tool_calls": tool_calls, "finish_reason": finish_reason, } @@ -278,6 +287,7 @@ class OpenAIClient(GenAIClient): logger.warning("OpenAI request timed out: %s", str(e)) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -285,6 +295,7 @@ class OpenAIClient(GenAIClient): logger.warning("OpenAI returned an error: %s", str(e)) return { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", } @@ -335,6 +346,7 @@ class OpenAIClient(GenAIClient): # Use streaming API content_parts: list[str] = [] + reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" usage_stats: Optional[dict[str, Any]] = None @@ -356,6 +368,15 @@ class OpenAIClient(GenAIClient): if choice.finish_reason: finish_reason = choice.finish_reason + # Extract reasoning deltas (reasoning_content or reasoning, + # depending on the server) + reasoning_delta = getattr(delta, "reasoning_content", None) or getattr( + delta, "reasoning", None + ) + if reasoning_delta: + reasoning_parts.append(reasoning_delta) + yield ("reasoning_delta", reasoning_delta) + # Extract content deltas if delta.content: content_parts.append(delta.content) @@ -384,6 +405,7 @@ class OpenAIClient(GenAIClient): # Build final message full_content = "".join(content_parts).strip() or None + full_reasoning = "".join(reasoning_parts).strip() or None # Convert tool calls to list format tool_calls_list = None @@ -412,6 +434,7 @@ class OpenAIClient(GenAIClient): "message", { "content": full_content, + "reasoning": full_reasoning, "tool_calls": tool_calls_list, "finish_reason": finish_reason, }, @@ -423,6 +446,7 @@ class OpenAIClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, @@ -433,6 +457,7 @@ class OpenAIClient(GenAIClient): "message", { "content": None, + "reasoning": None, "tool_calls": None, "finish_reason": "error", }, diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py new file mode 100644 index 0000000000..9ff81cdebd --- /dev/null +++ b/frigate/genai/prompts.py @@ -0,0 +1,739 @@ +"""Prompt and response-format builders for GenAI features. + +Centralizes the per-feature prompt framing and structured-output schema +shaping so provider clients in :mod:`frigate.genai.plugins` only handle +transport. +""" + +import datetime +from typing import Any, Dict, List, Optional + +from playhouse.shortcuts import model_to_dict + +from frigate.config import CameraConfig, FrigateConfig +from frigate.config.classification import ObjectClassificationType +from frigate.config.ui import UnitSystemEnum +from frigate.data_processing.post.types import ReviewMetadata +from frigate.models import Event + + +def build_review_description_prompt( + review_data: dict[str, Any], + thumbnails: list[bytes], + concerns: list[str], + preferred_language: str | None, + activity_context_prompt: str, +) -> str: + """Build the prompt for review activity description generation.""" + + def get_concern_prompt() -> str: + if concerns: + concern_list = "\n - ".join(concerns) + return ( + "\n- `other_concerns` (list of strings): Include a list of any of " + "the following concerns that are occurring:\n" + f" - {concern_list}" + ) + else: + return "" + + def get_language_prompt() -> str: + if preferred_language: + return f"Provide your answer in {preferred_language}" + else: + return "" + + def get_objects_list() -> str: + if review_data["unified_objects"]: + return "\n- " + "\n- ".join(review_data["unified_objects"]) + else: + return "\n- (No objects detected)" + + return f""" +Your task is to analyze a sequence of images taken in chronological order from a security camera. + +## Normal Activity Patterns for This Property + +{activity_context_prompt} + +## Task Instructions + +Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently. + +## Analysis Guidelines + +When forming your description: +- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list. +- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence. +- Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity). +- Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects. +- Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved. +- **Use the actual timestamp provided in "Activity started at"** below for time of day context—do not infer time from image brightness or darkness. Unusual hours (late night/early morning) should increase suspicion when the observable behavior itself appears questionable. However, recognize that some legitimate activities can occur at any hour. +- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible. +- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases. + +## Response Field Guidelines + +Respond with a JSON object matching the provided schema. Field-specific guidance: +- `observations`: Include the very start of the activity — for example, a vehicle entering the frame or pulling into the driveway — even if it lasts only a few frames and the rest of the clip is dominated by a longer activity. Include each arrival, departure, object handled, and notable change in position or state. Each item is a single concrete fact written as a complete sentence. +- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. For named subjects (those with a `←` separator in "Objects in Scene"), always use their name — do not replace them with generic terms. For unnamed objects (e.g., "person", "car"), refer to them naturally with articles (e.g., "a person", "the car"). Your description should align with and support the threat level you assign. +- `title`: Name the primary activity across the observations, together with the location. An activity is what is being done with objects, tools, or surfaces; locomotion through the scene qualifies as the activity only when no other interaction is observed. For named subjects, always use their name. For unnamed objects, refer to them naturally with articles. +- `shortSummary`: Briefly summarize the primary activity across the observations. +- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above. +{get_concern_prompt()} + +## Sequence Details + +- Camera: {review_data["camera"]} +- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest) +- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds +- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"} + +## Objects in Scene + +Each line represents a detection state, not necessarily unique individuals. The `←` symbol separates a recognized subject's name from their object type — use only the name (before the `←`) in your response, not the type after it. The same subject may appear across multiple lines if detected multiple times. + +**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.** +{get_objects_list()} + +{get_language_prompt()} +""" + + +def build_review_description_response_format(concerns: list[str]) -> dict[str, Any]: + """Build the structured-output JSON schema for review descriptions. + + Strips the `time` field (populated server-side) and drops + `other_concerns` when no concerns are configured. + """ + schema = ReviewMetadata.model_json_schema() + schema.get("properties", {}).pop("time", None) + + if "time" in schema.get("required", []): + schema["required"].remove("time") + if not concerns: + schema.get("properties", {}).pop("other_concerns", None) + if "other_concerns" in schema.get("required", []): + schema["required"].remove("other_concerns") + + return { + "type": "json_schema", + "json_schema": { + "name": "review_metadata", + "strict": True, + "schema": schema, + }, + } + + +def build_review_summary_prompt( + start_ts: float, + end_ts: float, + events: list[dict[str, Any]], + preferred_language: str | None, +) -> str: + """Build the prompt for a multi-event review summary.""" + time_range = ( + f"{datetime.datetime.fromtimestamp(start_ts).strftime('%B %d, %Y at %I:%M %p')}" + f" to " + f"{datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}" + ) + prompt = f""" +You are a security officer writing a concise security report. + +Time range: {time_range} + +Input format: Each event is a JSON object with: +- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time" +- "context": array of related events from other cameras that occurred during overlapping time periods + +**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.** + +Report Structure - Use this EXACT format: + +# Security Summary - {time_range} + +## Overview +[Write 1-2 sentences summarizing the overall activity pattern during this period.] + +--- + +## Timeline + +[Group events by time periods (e.g., "Morning (6:00 AM - 12:00 PM)", "Afternoon (12:00 PM - 5:00 PM)", "Evening (5:00 PM - 9:00 PM)", "Night (9:00 PM - 6:00 AM)"). Use appropriate time blocks based on when events occurred.] + +### [Time Block Name] + +**HH:MM AM/PM** | [Camera Name] | [Threat Level Indicator] +- [Event title]: [Clear description incorporating contextual information from the "context" array] +- Context: [If context array has items, mention them here, e.g., "Delivery truck present on Front Driveway Cam (HH:MM AM/PM)"] +- Assessment: [Brief assessment incorporating context - if context explains the event, note it here] + +[Repeat for each event in chronological order within the time block] + +--- + +## Summary +[One sentence summarizing the period. If all events are normal/explained: "Routine activity observed." If review needed: "Some activity requires review but no security concerns." If security concerns: "Security concerns requiring immediate attention."] + +Guidelines: +- List ALL events in chronological order, grouped by time blocks +- Threat level indicators: ✓ Normal, ⚠️ Needs review, 🔴 Security concern +- Integrate contextual information naturally - use the "context" array to enrich each event's description +- If context explains the event (e.g., delivery truck explains person at door), describe it accordingly (e.g., "delivery person" not "unidentified person") +- Be concise but informative - focus on what happened and what it means +- If contextual information makes an event clearly normal, reflect that in your assessment +- Only create time blocks that have events - don't create empty sections +""" + + prompt += "\n\nEvents:\n" + for event in events: + prompt += f"\n{event}\n" + + if preferred_language: + prompt += f"\nProvide your answer in {preferred_language}" + + return prompt + + +def build_object_description_prompt( + camera_config: CameraConfig, + event: Event, +) -> str: + """Build the prompt for a per-object description. + + Pulls the per-label override from `objects.genai.object_prompts`, falling + back to the camera default, and interpolates event fields. + + Raises: + KeyError: if the user-defined prompt template references an unknown + event field. + """ + template = camera_config.objects.genai.object_prompts.get( + str(event.label), + camera_config.objects.genai.prompt, + ) + return template.format(**model_to_dict(event)) + + +def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]: + """Return enabled custom classification models of `attribute` type. + + Each entry: {"name": , "objects": [, ...]}. + These models attach attribute metadata to events on the listed object + types, which can later be filtered via the search_objects `attribute` + field. + """ + result: List[Dict[str, Any]] = [] + + for model_key, model_config in config.classification.custom.items(): + if not model_config.enabled or model_config.object_config is None: + continue + + if ( + model_config.object_config.classification_type + != ObjectClassificationType.attribute + ): + continue + + result.append( + { + "name": model_config.name or model_key, + "objects": list(model_config.object_config.objects or []), + } + ) + + return result + + +def get_tool_definitions( + semantic_search_enabled: bool = False, + attribute_classifications: Optional[List[Dict[str, Any]]] = None, +) -> List[Dict[str, Any]]: + """ + Get OpenAI-compatible tool definitions for Frigate. + + Returns a list of tool definitions that can be used with OpenAI-compatible + function calling APIs. When semantic search is enabled, the search_objects + tool exposes an additional `semantic_query` parameter for descriptive + queries (e.g. "person riding a lawn mower") and find_similar_objects is + included. When attribute classification models are configured, an + `attribute` parameter is exposed for filtering by their labels. + """ + search_objects_properties: Dict[str, Any] = { + "camera": { + "type": "string", + "description": "Camera name to filter by (optional).", + }, + "label": { + "type": "string", + "description": ( + "Generic object class to filter by — one of the tracked detector " + "labels such as 'person', 'package', 'car', 'dog', 'bird'. Use " + "this for broad queries like 'show me all cars today'. Combine " + "with semantic_query when the user also describes appearance or " + "behavior (e.g. label='person', semantic_query='riding a lawn " + "mower')." + ), + }, + "sub_label": { + "type": "string", + "description": ( + "Filter by a DISCRETE NAMED entity recognized in the detection. " + "Use this for: a known person's name ('John'), a delivery " + "company ('Amazon', 'UPS'), a recognized animal species or " + "breed ('blue jay', 'cardinal', 'golden retriever'), or a " + "license plate string. When filtering by a specific name, set " + "only sub_label and leave label unset. Do NOT use sub_label " + "for descriptions of appearance, clothing, or actions — those " + "belong in semantic_query." + ), + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "List of zone names to filter by.", + }, + "limit": { + "type": "integer", + "description": "Maximum number of objects to return (default: 25).", + "default": 25, + }, + } + + if attribute_classifications: + model_outline = "; ".join( + f"{m['name']} (applies to {', '.join(m['objects']) or 'any object'})" + for m in attribute_classifications + ) + search_objects_properties["attribute"] = { + "type": "string", + "description": ( + "Filter by a classification attribute label produced by a " + "configured attribute classification model. Use this INSTEAD " + "of semantic_query when the user's request matches one of " + "these classifications. Configured models: " + f"{model_outline}. " + "Set the value to the attribute label that matches the user's " + "phrasing (case-sensitive)." + ), + } + + if semantic_search_enabled: + search_objects_properties["semantic_query"] = { + "type": "string", + "description": ( + "Optional natural-language description of a PHYSICAL " + "CHARACTERISTIC, APPEARANCE, or ACTIVITY the user mentioned, " + "used to semantically narrow results. Only set this when the " + "user describes something beyond what label and sub_label can " + "express on their own.\n" + "USE for descriptive phrases like: 'riding a lawn mower', " + "'wearing a red jacket', 'carrying a package', 'walking a " + "dog', 'on a bicycle', 'holding an umbrella'.\n" + "DO NOT USE for:\n" + "- specific named people, pets, or delivery companies → use sub_label\n" + "- animal species or breed names like 'blue jay', 'cardinal', " + "'golden retriever' → use sub_label\n" + "- license plate strings → use sub_label\n" + "- generic object queries like 'all cars today' or 'every " + "person' → use label alone with no semantic_query\n" + "When set, combine with label/time/camera/zone filters as " + "usual (e.g. label='person', semantic_query='riding a lawn " + "mower', after='2024-05-01T00:00:00Z')." + ), + } + + search_objects_description = ( + "Search the historical record of detected objects in Frigate. " + "Use this ONLY for questions about the PAST — e.g. 'did anyone come by today?', " + "'when was the last car?', 'show me detections from yesterday'. " + "Do NOT use this for monitoring or alerting requests about future events — " + "use start_camera_watch instead for those. " + "An 'object' in Frigate represents a tracked detection (e.g., a person, package, car).\n\n" + "Choose filters based on what the user is asking for:\n" + "- Generic class query ('show me all cars today'): set `label` only.\n" + "- Specific NAMED entity (known person, delivery company, animal " + "species/breed like 'blue jay' or 'golden retriever', license " + "plate): set `sub_label` only and leave `label` unset.\n" + ) + if semantic_search_enabled: + search_objects_description += ( + "- Physical CHARACTERISTIC, APPEARANCE, or ACTIVITY that is not a " + "discrete name ('person riding a lawn mower', 'someone in a red " + "jacket', 'person carrying a package'): set `semantic_query` with " + "the descriptive phrase, optionally alongside `label` for the " + "object class. Do NOT put descriptive phrases in sub_label." + ) + + return [ + { + "type": "function", + "function": { + "name": "search_objects", + "description": search_objects_description, + "parameters": { + "type": "object", + "properties": search_objects_properties, + }, + "required": [], + }, + }, + { + "type": "function", + "function": { + "name": "find_similar_objects", + "description": ( + "Find tracked objects that are visually and semantically similar " + "to a specific past event. Use this when the user references a " + "particular object they have seen and wants to find other " + "sightings of the same or similar one ('that green car', 'the " + "person in the red jacket', 'the package that was delivered'). " + "Prefer this over search_objects whenever the user's intent is " + "'find more like this specific one.' Use search_objects first " + "only if you need to locate the anchor event. Requires semantic " + "search to be enabled." + ), + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "description": "The id of the anchor event to find similar objects to.", + }, + "after": { + "type": "string", + "description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').", + }, + "before": { + "type": "string", + "description": "End time in ISO 8601 format (e.g., '2024-01-01T23:59:59Z').", + }, + "cameras": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of cameras to restrict to. Defaults to all.", + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of labels to restrict to. Defaults to the anchor event's label.", + }, + "sub_labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of sub_labels (names) to restrict to.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of zones. An event matches if any of its zones overlap.", + }, + "similarity_mode": { + "type": "string", + "enum": ["visual", "semantic", "fused"], + "description": "Which similarity signal(s) to use. 'fused' (default) combines visual and semantic.", + "default": "fused", + }, + "min_score": { + "type": "number", + "description": "Drop matches with a similarity score below this threshold (0.0-1.0).", + }, + "limit": { + "type": "integer", + "description": "Maximum number of matches to return (default: 10).", + "default": 10, + }, + }, + "required": ["event_id"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "set_camera_state", + "description": ( + "Change a camera's feature state (e.g., turn detection on/off, enable/disable recordings). " + "Use camera='*' to apply to all cameras at once. " + "Only call this tool when the user explicitly asks to change a camera setting. " + "Requires admin privileges." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera name to target, or '*' to target all cameras.", + }, + "feature": { + "type": "string", + "enum": [ + "detect", + "record", + "snapshots", + "audio", + "motion", + "enabled", + "birdseye", + "birdseye_mode", + "improve_contrast", + "ptz_autotracker", + "motion_contour_area", + "motion_threshold", + "notifications", + "audio_transcription", + "review_alerts", + "review_detections", + "object_descriptions", + "review_descriptions", + "profile", + ], + "description": ( + "The feature to change. Most features accept ON or OFF. " + "birdseye_mode accepts CONTINUOUS, MOTION, or OBJECTS. " + "motion_contour_area and motion_threshold accept a number. " + "profile accepts a profile name or 'none' to deactivate (requires camera='*')." + ), + }, + "value": { + "type": "string", + "description": "The value to set. ON or OFF for toggles, a number for thresholds, a profile name or 'none' for profile.", + }, + }, + "required": ["camera", "feature", "value"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_live_context", + "description": ( + "Get the current live image and detection information for a camera: objects being tracked, " + "zones, timestamps. Use this to understand what is visible in the live view. " + "Call this when answering questions about what is happening right now on a specific camera." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera name to get live context for.", + }, + }, + "required": ["camera"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "start_camera_watch", + "description": ( + "Start a continuous VLM watch job that monitors a camera and sends a notification " + "when a specified condition is met. Use this when the user wants to be alerted about " + "a future event, e.g. 'tell me when guests arrive' or 'notify me when the package is picked up'. " + "Only one watch job can run at a time. Returns a job ID." + ), + "parameters": { + "type": "object", + "properties": { + "camera": { + "type": "string", + "description": "Camera ID to monitor.", + }, + "condition": { + "type": "string", + "description": ( + "Natural-language description of the condition to watch for, " + "e.g. 'a person arrives at the front door'." + ), + }, + "max_duration_minutes": { + "type": "integer", + "description": "Maximum time to watch before giving up (minutes, default 60).", + "default": 60, + }, + "labels": { + "type": "array", + "items": {"type": "string"}, + "description": "Object labels that should trigger a VLM check (e.g. ['person', 'car']). If omitted, any detection on the camera triggers a check.", + }, + "zones": { + "type": "array", + "items": {"type": "string"}, + "description": "Zone names to filter by. If specified, only detections in these zones trigger a VLM check.", + }, + }, + "required": ["camera", "condition"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "stop_camera_watch", + "description": ( + "Cancel the currently running VLM watch job. Use this when the user wants to " + "stop a previously started watch, e.g. 'stop watching the front door'." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_profile_status", + "description": ( + "Get the current profile status including the active profile and " + "timestamps of when each profile was last activated. Use this to " + "determine time periods for recap requests — e.g. when the user asks " + "'what happened while I was away?', call this first to find the relevant " + "time window based on profile activation history." + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + { + "type": "function", + "function": { + "name": "get_recap", + "description": ( + "Get a recap of all activity (alerts and detections) for a given time period. " + "Use this after calling get_profile_status to retrieve what happened during " + "a specific window — e.g. 'what happened while I was away?'. Returns a " + "chronological list of activity with camera, objects, zones, and GenAI-generated " + "descriptions when available. Summarize the results for the user." + ), + "parameters": { + "type": "object", + "properties": { + "after": { + "type": "string", + "description": "Start of the time period in ISO 8601 format (e.g. '2025-03-15T08:00:00').", + }, + "before": { + "type": "string", + "description": "End of the time period in ISO 8601 format (e.g. '2025-03-15T17:00:00').", + }, + "cameras": { + "type": "string", + "description": "Comma-separated camera IDs to include, or 'all' for all cameras. Default is 'all'.", + }, + "severity": { + "type": "string", + "enum": ["alert", "detection"], + "description": "Filter by severity level. Omit to include both alerts and detections.", + }, + }, + "required": ["after", "before"], + }, + }, + }, + ] + + +def build_chat_system_prompt( + config: FrigateConfig, + allowed_cameras: List[str], + semantic_search_enabled: bool, + attribute_classifications: List[Dict[str, Any]], +) -> str: + """Build the system prompt for the chat completion endpoint. + + Composes the static framing with conditional sections describing the + available cameras, speed units, semantic-search routing guidance, and + configured attribute classifications. + """ + current_datetime = datetime.datetime.now() + current_date_str = current_datetime.strftime("%Y-%m-%d") + current_time_str = current_datetime.strftime("%I:%M:%S %p") + + cameras_info: List[str] = [] + has_speed_zone = False + for camera_id in allowed_cameras: + if camera_id not in config.cameras: + continue + camera_config = config.cameras[camera_id] + friendly_name = ( + camera_config.friendly_name + if camera_config.friendly_name + else camera_id.replace("_", " ").title() + ) + zone_names = list(camera_config.zones.keys()) + if not has_speed_zone: + has_speed_zone = any( + zone.distances for zone in camera_config.zones.values() + ) + if zone_names: + cameras_info.append( + f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" + ) + else: + cameras_info.append(f" - {friendly_name} (ID: {camera_id})") + + cameras_section = "" + if cameras_info: + cameras_section = ( + "\n\nAvailable cameras:\n" + + "\n".join(cameras_info) + + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." + ) + + speed_units_section = "" + if has_speed_zone: + speed_unit = ( + "mph" if config.ui.unit_system == UnitSystemEnum.imperial else "km/h" + ) + speed_units_section = f"\n\nReport object speeds to the user in {speed_unit}." + + semantic_search_section = "" + if semantic_search_enabled: + semantic_search_section = ( + "\n\nWhen routing a search_objects call, pick filters by the shape of the user's request:\n" + "- Generic class ('show me all cars today'): set `label` only.\n" + "- Specific named entity — a known person ('John'), delivery company ('Amazon'), animal species/breed ('blue jay', 'cardinal', 'golden retriever'), or license plate: set `sub_label` only and leave `label` unset.\n" + "- Physical characteristic, appearance, or activity that is NOT a discrete name ('find me people riding a lawn mower', 'someone in a red jacket', 'a person carrying a package'): set `semantic_query` with the descriptive phrase, optionally combined with `label` for the object class. Never put descriptive phrases in `sub_label`." + ) + + attribute_classification_section = "" + if attribute_classifications: + model_lines = "\n".join( + f"- {m['name']}: applies to {', '.join(m['objects']) or 'any object'}" + for m in attribute_classifications + ) + attribute_classification_section = ( + "\n\nAttribute classification models are configured for the following object types:\n" + f"{model_lines}\n" + "When the user's request matches one of these classifications, set the search_objects `attribute` field to the matching label rather than using `semantic_query`. Reserve `semantic_query` for descriptive phrases that fall outside the configured attribute labels." + ) + + return f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events. + +Current server local date and time: {current_date_str} at {current_time_str} + +Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly. + +Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields. +When users ask about "today", "yesterday", "this week", etc., use the current date above as reference. +When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today). +Always be accurate with time calculations based on the current date provided. + +When a user refers to a specific object they have seen or describe with identifying details ("that green car", "the person in the red jacket", "a package left today"), prefer the find_similar_objects tool over search_objects. Use search_objects first only to locate the anchor event, then pass its id to find_similar_objects. For generic queries like "show me all cars today", keep using search_objects. If a user message begins with [attached_event:], treat that event id as the anchor for any similarity or "tell me more" request in the same message and call find_similar_objects with that id.{semantic_search_section}{attribute_classification_section}{cameras_section}{speed_units_section}""" diff --git a/web/public/locales/en/views/chat.json b/web/public/locales/en/views/chat.json index bc320c2049..9e68551f03 100644 --- a/web/public/locales/en/views/chat.json +++ b/web/public/locales/en/views/chat.json @@ -60,5 +60,10 @@ "stats": { "context": "{{tokens}} tokens", "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Reasoning…", + "show": "Show reasoning", + "hide": "Hide reasoning" } } diff --git a/web/src/components/chat/ReasoningBubble.tsx b/web/src/components/chat/ReasoningBubble.tsx new file mode 100644 index 0000000000..dd7c8fe819 --- /dev/null +++ b/web/src/components/chat/ReasoningBubble.tsx @@ -0,0 +1,87 @@ +import { useState, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { LuBrain, LuChevronDown, LuChevronRight } from "react-icons/lu"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type ReasoningBubbleProps = { + /** The accumulated reasoning text from the model. */ + reasoning: string; + /** + * Whether the assistant has begun producing the user-facing answer. + * While false the reasoning is still streaming and we keep the panel + * open with a "Thinking…" label. Once true, the panel auto-collapses + * so the answer is the primary focus, but stays expandable. + */ + answerStarted: boolean; +}; + +export function ReasoningBubble({ + reasoning, + answerStarted, +}: ReasoningBubbleProps) { + const { t } = useTranslation(["views/chat"]); + // Open while the model is still mid-thought (no answer tokens yet); + // once the answer begins, collapse on its own but let the user reopen. + const [open, setOpen] = useState(true); + const userInteractedRef = useRef(false); + const lastAutoState = useRef(true); + + useEffect(() => { + if (userInteractedRef.current) return; + const desired = !answerStarted; + if (desired !== lastAutoState.current) { + lastAutoState.current = desired; + setOpen(desired); + } + }, [answerStarted]); + + const handleOpenChange = (next: boolean) => { + userInteractedRef.current = true; + setOpen(next); + }; + + const label = !answerStarted + ? t("reasoning.active") + : open + ? t("reasoning.hide") + : t("reasoning.show"); + + return ( +
+ + + + + +
+            {reasoning}
+          
+
+
+
+ ); +} diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index 970fa3d364..4621c97540 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -7,6 +7,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import axios from "axios"; import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { MessageBubble } from "@/components/chat/ChatMessage"; +import { ReasoningBubble } from "@/components/chat/ReasoningBubble"; import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup"; import { ChatStartingState } from "@/components/chat/ChatStartingState"; import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip"; @@ -200,15 +201,21 @@ export default function ChatPage() { const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0; const hasContent = !!msg.content?.trim(); + const hasReasoning = !!msg.reasoning?.trim(); const showProcessing = - isLastAssistant && isLoading && !hasContent; + isLastAssistant && + isLoading && + !hasContent && + !hasReasoning; - // Hide empty placeholder only when there are no tool calls yet + // Hide empty placeholder only when there are no tool calls + // and no reasoning streaming yet if ( isLastAssistant && isLoading && !hasContent && - !hasToolCalls + !hasToolCalls && + !hasReasoning ) return (
)} + {msg.role === "assistant" && hasReasoning && ( + + )} {showProcessing ? (
- ) : ( + ) : msg.role === "assistant" && + !hasContent && + hasReasoning && + !isComplete ? null : ( { + const next = [...prev]; + const lastMsg = next[next.length - 1]; + if (lastMsg?.role === "assistant") + next[next.length - 1] = { + ...lastMsg, + reasoning: (lastMsg.reasoning ?? "") + data.delta, + }; + return next; + }); + return "continue"; + } if (data.type === "stats") { const stats: ChatStats = { promptTokens: data.prompt_tokens, From 7881bea60f8cee434939914c18c4479f245941bd Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 19 May 2026 14:51:16 -0500 Subject: [PATCH 10/94] Filter outbound websocket broadcasts by per-recipient camera access (#23256) * filter outbound ws broadcasts by per-recipient camera access * fan out config updates to comms * tests * mypy * allow viewers to use jobstate * update agent instructions * remove vitest --- .github/copilot-instructions.md | 40 +- frigate/api/app.py | 2 + frigate/comms/ws.py | 362 ++++++++++- frigate/test/test_ws_outbound_filter.py | 806 ++++++++++++++++++++++++ 4 files changed, 1203 insertions(+), 7 deletions(-) create mode 100644 frigate/test/test_ws_outbound_filter.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0af9c249f1..d87dbb239e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -162,7 +162,6 @@ When reviewing code, do NOT comment on: - **Linting**: ESLint (see `web/.eslintrc.cjs`) - **Formatting**: Prettier with Tailwind CSS plugin - **Type Safety**: TypeScript strict mode enabled -- **Testing**: Vitest for unit tests ### Component Patterns @@ -233,6 +232,9 @@ ruff format frigate/ # Run linter ruff check frigate/ + +# Type check +python3 -u -m mypy --config-file frigate/mypy.ini frigate ``` ### Frontend (from web/ directory) @@ -252,6 +254,38 @@ npm run lint:fix # Format code npm run prettier:write + +# E2E: first-time setup +npm install +npx playwright install chromium + +# E2E: build the app and run all tests +npm run e2e:build && npm run e2e + +# E2E: interactive UI for debugging +npm run e2e:ui + +# E2E: run a specific spec +npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts + +# E2E: filter by name, or run only desktop/mobile +npx playwright test --config e2e/playwright.config.ts --grep="severity tab" +npx playwright test --config e2e/playwright.config.ts --project=desktop + +# E2E: regenerate mock data after backend model changes (from repo root) +PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py + +# Regenerate config translations from Pydantic models — outputs to +# web/public/locales/en/config/{global,cameras}.json. NEVER edit those +# JSON files by hand; change the Pydantic field title/description and +# re-run this script. (from repo root) +python3 generate_config_translations.py + +# Extract i18n keys from source into the locale files after adding +# new t() calls. Use the :ci variant to verify the locale files are +# in sync with source (fails if extraction would change anything). +npm run i18n:extract +npm run i18n:extract:ci ``` ### Docker Development @@ -371,6 +405,10 @@ except ValueError: ) ``` +## WebSocket Broadcasts + +Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear. + ## Project-Specific Conventions ### Configuration Files diff --git a/frigate/api/app.py b/frigate/api/app.py index 9ff24ed7e8..4fac58a715 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -774,6 +774,8 @@ def config_set(request: Request, body: AppConfigSetBody): if request.app.dispatcher is not None: request.app.dispatcher.config = config + for comm in request.app.dispatcher.comms: + comm.config = config if body.update_topic: if body.update_topic.startswith("config/cameras/"): diff --git a/frigate/comms/ws.py b/frigate/comms/ws.py index 2f16ab7141..5b555999e3 100644 --- a/frigate/comms/ws.py +++ b/frigate/comms/ws.py @@ -34,6 +34,8 @@ from frigate.const import ( UPDATE_REVIEW_DESCRIPTION, UPSERT_REVIEW_SEGMENT, ) +from frigate.models import User +from frigate.output.ws_auth import ws_has_camera_access logger = logging.getLogger(__name__) @@ -66,6 +68,7 @@ _WS_VIEWER_TOPICS = frozenset( "audioTranscriptionState", "birdseyeLayout", "embeddingsReindexProgress", + "jobState", } ) @@ -102,6 +105,321 @@ def _check_ws_authorization( return topic in _WS_VIEWER_TOPICS +# ---- Outbound filtering --------------------------------------------------- +# +# Every WebSocket broadcast is classified into one of a small set of scopes, +# then materialized per recipient. Connections with restricted roles only see +# data for cameras they are authorized to access; admin and full-access roles +# behave as today. + +# Topics that are safe to broadcast to every authenticated client. +_WS_GLOBAL_OUTBOUND_TOPICS = frozenset( + { + "model_state", + "embeddings_reindex_progress", + "audio_transcription_state", + "profile/state", + "notifications/state", + "notification_test", + } +) + +# Topics that restricted roles must never receive. Birdseye composites span +# all cameras, so the existing JSMPEG policy already restricts birdseye access +# to unrestricted roles; the layout broadcast follows the same rule. +_WS_UNRESTRICTED_ONLY_TOPICS = frozenset( + { + "birdseye_layout", + } +) + +# Topics whose payload (parsed as JSON) names a single owning camera at the +# given key path. Used to scope events, reviews, triggers, etc. +_WS_PAYLOAD_CAMERA_TOPICS: dict[str, tuple[str, ...]] = { + "events": ("after", "camera"), + "reviews": ("after", "camera"), + "tracked_object_update": ("camera",), + "triggers": ("camera",), + "camera_monitoring": ("camera",), +} + +# Topics whose payload is a dict keyed by camera name; filter keys per +# recipient. +_WS_RESHAPE_BY_CAMERA_KEY_TOPICS = frozenset( + { + "camera_activity", + "audio_detections", + } +) + +# Topics whose payload is a dict keyed by job_type, where each entry may +# contain a "camera" or "source_camera" field, or a nested ``results.jobs`` +# list of per-camera sub-jobs (export broadcasts). +_WS_RESHAPE_JOB_STATE_TOPICS = frozenset( + { + "job_state", + } +) + +# Topics whose payload mixes global aggregates with a ``cameras`` sub-dict +# keyed by camera name. Aggregates and detector data stay; per-camera entries +# are filtered. +_WS_RESHAPE_STATS_TOPICS = frozenset( + { + "stats", + } +) + + +def _collect_zone_names(config: FrigateConfig) -> set[str]: + """Return the set of all zone names defined across cameras.""" + names: set[str] = set() + for camera in config.cameras.values(): + zones = getattr(camera, "zones", None) or {} + names.update(zones.keys()) + return names + + +def _parse_json_payload(payload: Any) -> Any: + """Return payload parsed as JSON if it is a string, else as-is.""" + if isinstance(payload, str): + try: + return json.loads(payload) + except (ValueError, TypeError): + return None + return payload + + +def _scope_job_entry_to_allowed(entry: Any, allowed: set[str]) -> dict[str, Any] | None: + """Filter a single job_state entry to the recipient's allowed cameras. + + Returns the (possibly reshaped) entry, or None to drop it. Four shapes + are handled: + + * Top-level ``camera`` or ``source_camera`` (motion_search, vlm_watch, + export sub-job dicts): drop the entry if not allowed. + * Nested ``results.jobs`` list of per-camera sub-jobs (the aggregated + export broadcast): filter the list; drop the entry if nothing remains. + * Nested ``results.camera`` or ``results.source_camera`` (debug_replay, + which puts replay-specific fields inside ``results``): drop the entry + if not allowed. + * No camera anywhere (e.g. ``media_sync``): treat as global and keep. + """ + if not isinstance(entry, dict): + return None + + cam = entry.get("camera") or entry.get("source_camera") + + if cam is None: + results = entry.get("results") + if isinstance(results, dict): + sub_jobs = results.get("jobs") + if isinstance(sub_jobs, list): + filtered_jobs = [ + j + for j in sub_jobs + if isinstance(j, dict) + and (j.get("camera") or j.get("source_camera")) in allowed + ] + if not filtered_jobs: + return None + reshaped = dict(entry) + reshaped["results"] = dict(results) + reshaped["results"]["jobs"] = filtered_jobs + return reshaped + + cam = results.get("camera") or results.get("source_camera") + + if cam is not None: + return entry if cam in allowed else None + + return entry + + +def _extract_payload_camera(payload: Any, path: tuple[str, ...]) -> str | None: + """Walk the dotted path through a (possibly JSON-encoded) payload.""" + cur = _parse_json_payload(payload) + for key in path: + if not isinstance(cur, dict): + return None + cur = cur.get(key) + return cur if isinstance(cur, str) else None + + +def _classify_outbound( + topic: str, all_cameras: set[str], all_zones: set[str] +) -> tuple[str, Any]: + """Classify an outbound topic into (kind, extra). + + kind values: + - "global" : send to every authenticated client + - "drop" : send to nobody (fail-closed for unknowns) + - "unrestricted_only" : send only to admin/full-access roles + - "camera" : extra is the owning camera name + - "payload_camera" : extra is the JSON key path to the camera name + - "reshape_by_camera_key" + - "reshape_job_state" + - "reshape_stats" + """ + if topic in _WS_GLOBAL_OUTBOUND_TOPICS: + return ("global", None) + if topic in _WS_UNRESTRICTED_ONLY_TOPICS: + return ("unrestricted_only", None) + if topic in _WS_RESHAPE_BY_CAMERA_KEY_TOPICS: + return ("reshape_by_camera_key", None) + if topic in _WS_RESHAPE_JOB_STATE_TOPICS: + return ("reshape_job_state", None) + if topic in _WS_RESHAPE_STATS_TOPICS: + return ("reshape_stats", None) + if topic in _WS_PAYLOAD_CAMERA_TOPICS: + return ("payload_camera", _WS_PAYLOAD_CAMERA_TOPICS[topic]) + + # Topic-prefix based: first segment names the owning camera or zone. + first = topic.split("/", 1)[0] + if first in all_cameras: + return ("camera", first) + if first in all_zones: + # Zone aggregates span cameras; restricted users see nothing here. + return ("unrestricted_only", None) + + return ("drop", None) + + +def _ws_role_header(ws: Any) -> str | None: + """Return the HTTP_REMOTE_ROLE header value, if any.""" + environ = getattr(ws, "environ", None) + if not environ: + return None + value = environ.get("HTTP_REMOTE_ROLE") + return value if isinstance(value, str) else None + + +def _ws_valid_roles(ws: Any, config: FrigateConfig) -> list[str]: + """Return the list of recognized roles for this connection.""" + header = _ws_role_header(ws) + if not header: + return [] + roles = [r.strip() for r in header.split(config.proxy.separator) if r.strip()] + return [r for r in roles if r in config.auth.roles] + + +def _ws_is_unrestricted(ws: Any, config: FrigateConfig) -> bool: + """True when the connection has unrestricted camera access. + + Mirrors the policy in ``frigate.output.ws_auth``: admin or any role with + an empty allow-list grants full access. + """ + roles = _ws_valid_roles(ws, config) + if not roles: + return False + roles_dict = config.auth.roles + return any(r == "admin" or not roles_dict.get(r) for r in roles) + + +def _ws_allowed_cameras(ws: Any, config: FrigateConfig) -> set[str]: + """Return the union of cameras this connection may access across its roles.""" + roles = _ws_valid_roles(ws, config) + if not roles: + return set() + all_cameras = set(config.cameras.keys()) + allowed: set[str] = set() + for role in roles: + if role == "admin" or not config.auth.roles.get(role): + return all_cameras + allowed.update(User.get_allowed_cameras(role, config.auth.roles, all_cameras)) + return allowed + + +def _wrap_envelope(topic: str, inner_payload: Any) -> str: + """Re-serialize a (topic, payload) message after payload reshaping. + + Frigate's wire format keeps payloads as JSON-encoded strings inside the + outer envelope, mirroring what producers send today. + """ + return json.dumps({"topic": topic, "payload": json.dumps(inner_payload)}) + + +def _materialize_for_ws( + ws: Any, + topic: str, + full_message: str, + scope: tuple[str, Any], + parsed_payload: Any, + config: FrigateConfig, +) -> str | None: + """Return the JSON string to deliver to ``ws``, or None to skip it.""" + kind, extra = scope + has_role = _ws_role_header(ws) is not None + + if kind == "drop": + return None + + if kind == "global": + # Globals still require an authenticated connection. Missing role + # falls back to viewer semantics (matching the inbound rule). + return full_message + + # Beyond globals, an authenticated role header is required (fail-closed). + if not has_role: + return None + + if kind == "unrestricted_only": + return full_message if _ws_is_unrestricted(ws, config) else None + + if kind == "camera": + return full_message if ws_has_camera_access(ws, extra, config) else None + + if kind == "payload_camera": + camera = _extract_payload_camera(parsed_payload, extra) + if camera is None: + return None + return full_message if ws_has_camera_access(ws, camera, config) else None + + if kind == "reshape_by_camera_key": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + filtered = {cam: data for cam, data in parsed_payload.items() if cam in allowed} + if not filtered: + return None + return _wrap_envelope(topic, filtered) + + if kind == "reshape_job_state": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + filtered_jobs: dict[str, Any] = {} + for job_type, job_payload in parsed_payload.items(): + scoped = _scope_job_entry_to_allowed(job_payload, allowed) + if scoped is not None: + filtered_jobs[job_type] = scoped + if not filtered_jobs: + return None + return _wrap_envelope(topic, filtered_jobs) + + if kind == "reshape_stats": + if _ws_is_unrestricted(ws, config): + return full_message + if not isinstance(parsed_payload, dict): + return None + allowed = _ws_allowed_cameras(ws, config) + cameras_block = parsed_payload.get("cameras") + if isinstance(cameras_block, dict): + filtered_cameras = { + name: data for name, data in cameras_block.items() if name in allowed + } + reshaped = dict(parsed_payload) + reshaped["cameras"] = filtered_cameras + return _wrap_envelope(topic, reshaped) + return full_message + + return None + + class WebSocket(WebSocket_): # type: ignore[misc] def unhandled_error(self, error: Any) -> None: """ @@ -183,6 +501,10 @@ class WebSocketClient(Communicator): self.websocket_thread.start() def publish(self, topic: str, payload: Any, _: bool = False) -> None: + if self.websocket_server is None: + logger.debug("Skipping message, websocket not connected yet") + return + try: ws_message = json.dumps( { @@ -195,14 +517,42 @@ class WebSocketClient(Communicator): logger.debug(f"payload for {topic} wasn't text. Skipping...") return - if self.websocket_server is None: - logger.debug("Skipping message, websocket not connected yet") + all_cameras = set(self.config.cameras.keys()) + all_zones = _collect_zone_names(self.config) + scope = _classify_outbound(topic, all_cameras, all_zones) + + if scope[0] == "drop": return - try: - self.websocket_server.manager.broadcast(ws_message) - except ConnectionResetError: - pass + # Pre-parse payload once for topics that need to read its contents. + parsed_payload: Any = None + if scope[0] in ( + "payload_camera", + "reshape_by_camera_key", + "reshape_job_state", + "reshape_stats", + ): + parsed_payload = _parse_json_payload(payload) + if parsed_payload is None: + # malformed payload — fail closed + return + + manager = self.websocket_server.manager + with manager.lock: + websockets = list(manager.websockets.values()) + + for ws in websockets: + if getattr(ws, "terminated", False): + continue + message = _materialize_for_ws( + ws, topic, ws_message, scope, parsed_payload, self.config + ) + if message is None: + continue + try: + ws.send(message) + except (ConnectionResetError, BrokenPipeError, ValueError): + pass def stop(self) -> None: if self.websocket_server is not None: diff --git a/frigate/test/test_ws_outbound_filter.py b/frigate/test/test_ws_outbound_filter.py new file mode 100644 index 0000000000..ab1489da54 --- /dev/null +++ b/frigate/test/test_ws_outbound_filter.py @@ -0,0 +1,806 @@ +"""Tests for outbound WebSocket broadcast filtering.""" + +import json +import threading +import unittest +from types import SimpleNamespace +from typing import Any + +from frigate.comms.ws import ( + WebSocketClient, + _classify_outbound, + _collect_zone_names, + _extract_payload_camera, + _materialize_for_ws, + _ws_allowed_cameras, + _ws_is_unrestricted, +) +from frigate.config import FrigateConfig + + +def _build_config( + *, + extra_roles: dict[str, list[str]] | None = None, + extra_cameras: dict[str, dict[str, Any]] | None = None, + extra_zones: dict[str, dict[str, dict[str, Any]]] | None = None, +) -> FrigateConfig: + """Construct a FrigateConfig used by the outbound filter tests. + + The default fixture has three cameras: front_door, back_door, garage. + Restricted role "house_only" sees front_door + back_door but not garage. + """ + cameras: dict[str, dict[str, Any]] = { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "garage": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.3:554/v", "roles": ["detect"]}], + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + } + if extra_cameras: + cameras.update(extra_cameras) + if extra_zones: + for cam_name, zones in extra_zones.items(): + cameras[cam_name]["zones"] = zones + + roles = {"house_only": ["front_door", "back_door"]} + if extra_roles: + roles.update(extra_roles) + + return FrigateConfig( + mqtt={"host": "mqtt"}, + auth={"roles": roles}, + cameras=cameras, + ) + + +def _ws(role: str | None) -> Any: + """Build a fake ws4py-style websocket exposing ``environ``.""" + environ = {} if role is None else {"HTTP_REMOTE_ROLE": role} + return SimpleNamespace(environ=environ, terminated=False, sent=[]) + + +class TestClassifyOutbound(unittest.TestCase): + """The pure classifier — bucket every topic into a scope.""" + + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.all_cameras = set(self.config.cameras.keys()) + self.all_zones = _collect_zone_names(self.config) + + def _classify(self, topic: str) -> tuple[str, Any]: + return _classify_outbound(topic, self.all_cameras, self.all_zones) + + # --- Global allowlist --- + + def test_model_state_is_global(self): + self.assertEqual(self._classify("model_state"), ("global", None)) + + def test_profile_state_is_global(self): + self.assertEqual(self._classify("profile/state"), ("global", None)) + + def test_bare_notifications_state_is_global(self): + """The 2-segment ``notifications/state`` is global; the 3-segment + ``/notifications/state`` is camera-scoped (see below).""" + self.assertEqual(self._classify("notifications/state"), ("global", None)) + + def test_notification_test_is_global(self): + self.assertEqual(self._classify("notification_test"), ("global", None)) + + # --- Unrestricted-only --- + + def test_birdseye_layout_is_unrestricted_only(self): + self.assertEqual(self._classify("birdseye_layout"), ("unrestricted_only", None)) + + # --- Camera-prefixed --- + + def test_camera_state_topic_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/detect/state"), ("camera", "front_door") + ) + + def test_camera_motion_topic_resolves_to_camera(self): + self.assertEqual(self._classify("back_door/motion"), ("camera", "back_door")) + + def test_camera_per_notification_topic_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/notifications/state"), + ("camera", "front_door"), + ) + + def test_camera_label_counter_resolves_to_camera(self): + self.assertEqual(self._classify("front_door/person"), ("camera", "front_door")) + + def test_camera_object_mask_state_resolves_to_camera(self): + self.assertEqual( + self._classify("front_door/object_mask/zone_1/state"), + ("camera", "front_door"), + ) + + # --- Zone-prefixed --- + + def test_zone_aggregate_topic_is_unrestricted_only(self): + self.assertEqual(self._classify("driveway/person"), ("unrestricted_only", None)) + + def test_zone_all_topic_is_unrestricted_only(self): + self.assertEqual(self._classify("driveway/all"), ("unrestricted_only", None)) + + # --- Payload-camera --- + + def test_events_topic_marks_payload_camera_path(self): + self.assertEqual( + self._classify("events"), ("payload_camera", ("after", "camera")) + ) + + def test_reviews_topic_marks_payload_camera_path(self): + self.assertEqual( + self._classify("reviews"), ("payload_camera", ("after", "camera")) + ) + + def test_triggers_topic_marks_payload_camera_path(self): + self.assertEqual(self._classify("triggers"), ("payload_camera", ("camera",))) + + def test_tracked_object_update_marks_payload_camera_path(self): + self.assertEqual( + self._classify("tracked_object_update"), ("payload_camera", ("camera",)) + ) + + # --- Reshape --- + + def test_camera_activity_is_reshape_by_camera_key(self): + self.assertEqual( + self._classify("camera_activity"), ("reshape_by_camera_key", None) + ) + + def test_audio_detections_is_reshape_by_camera_key(self): + self.assertEqual( + self._classify("audio_detections"), ("reshape_by_camera_key", None) + ) + + def test_job_state_is_reshape_job_state(self): + self.assertEqual(self._classify("job_state"), ("reshape_job_state", None)) + + def test_stats_is_reshape_stats(self): + self.assertEqual(self._classify("stats"), ("reshape_stats", None)) + + # --- Fail-closed --- + + def test_unknown_topic_is_dropped(self): + self.assertEqual(self._classify("some_random_topic"), ("drop", None)) + + def test_unknown_camera_prefix_is_dropped(self): + self.assertEqual(self._classify("ghost_camera/detect/state"), ("drop", None)) + + +class TestCollectZoneNames(unittest.TestCase): + def test_zones_from_all_cameras(self): + config = _build_config( + extra_zones={ + "front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}, + "back_door": {"yard": {"coordinates": "0,0,1,0,1,1,0,1"}}, + } + ) + self.assertEqual(_collect_zone_names(config), {"driveway", "yard"}) + + def test_no_zones_returns_empty(self): + self.assertEqual(_collect_zone_names(_build_config()), set()) + + +class TestExtractPayloadCamera(unittest.TestCase): + def test_extract_from_dict_path(self): + payload = {"after": {"camera": "front_door"}} + self.assertEqual( + _extract_payload_camera(payload, ("after", "camera")), "front_door" + ) + + def test_extract_from_json_string(self): + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertEqual( + _extract_payload_camera(payload, ("after", "camera")), "front_door" + ) + + def test_extract_single_segment_path(self): + self.assertEqual( + _extract_payload_camera({"camera": "garage"}, ("camera",)), "garage" + ) + + def test_missing_key_returns_none(self): + self.assertIsNone(_extract_payload_camera({}, ("after", "camera"))) + + def test_malformed_json_returns_none(self): + self.assertIsNone(_extract_payload_camera("not-json", ("camera",))) + + def test_non_string_camera_returns_none(self): + self.assertIsNone(_extract_payload_camera({"camera": 42}, ("camera",))) + + +class TestWsRoleHelpers(unittest.TestCase): + def setUp(self): + self.config = _build_config() + + def test_admin_is_unrestricted(self): + self.assertTrue(_ws_is_unrestricted(_ws("admin"), self.config)) + + def test_viewer_is_unrestricted(self): + self.assertTrue(_ws_is_unrestricted(_ws("viewer"), self.config)) + + def test_restricted_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws("house_only"), self.config)) + + def test_missing_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws(None), self.config)) + + def test_unknown_role_is_not_unrestricted(self): + self.assertFalse(_ws_is_unrestricted(_ws("ghost"), self.config)) + + def test_admin_allowed_cameras_is_all(self): + self.assertEqual( + _ws_allowed_cameras(_ws("admin"), self.config), + {"front_door", "back_door", "garage"}, + ) + + def test_restricted_role_allowed_cameras_is_subset(self): + self.assertEqual( + _ws_allowed_cameras(_ws("house_only"), self.config), + {"front_door", "back_door"}, + ) + + def test_missing_role_allowed_cameras_is_empty(self): + self.assertEqual(_ws_allowed_cameras(_ws(None), self.config), set()) + + def test_multi_role_union_grants_widest(self): + self.assertEqual( + _ws_allowed_cameras(_ws("house_only,admin"), self.config), + {"front_door", "back_door", "garage"}, + ) + + +class TestMaterializeForWs(unittest.TestCase): + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.all_cameras = set(self.config.cameras.keys()) + self.all_zones = _collect_zone_names(self.config) + + def _materialize(self, ws: Any, topic: str, payload: Any) -> str | None: + scope = _classify_outbound(topic, self.all_cameras, self.all_zones) + from frigate.comms.ws import _parse_json_payload + + parsed = ( + _parse_json_payload(payload) + if scope[0] + in ( + "payload_camera", + "reshape_by_camera_key", + "reshape_job_state", + "reshape_stats", + ) + else None + ) + full = json.dumps({"topic": topic, "payload": payload}) + return _materialize_for_ws(ws, topic, full, scope, parsed, self.config) + + # --- Globals: every authenticated client sees them --- + + def test_globals_reach_admin(self): + self.assertIsNotNone(self._materialize(_ws("admin"), "model_state", "{}")) + + def test_globals_reach_restricted(self): + self.assertIsNotNone(self._materialize(_ws("house_only"), "model_state", "{}")) + + def test_globals_reach_no_role(self): + """A missing role header still gets globals (matches viewer-default + for inbound).""" + self.assertIsNotNone(self._materialize(_ws(None), "model_state", "{}")) + + # --- Unknown topic dropped for everyone --- + + def test_unknown_topic_dropped_for_admin(self): + self.assertIsNone(self._materialize(_ws("admin"), "rogue_topic", "{}")) + + # --- Non-global topics require a role (fail-closed) --- + + def test_no_role_blocked_from_camera_topic(self): + self.assertIsNone(self._materialize(_ws(None), "front_door/detect/state", "ON")) + + def test_no_role_blocked_from_events(self): + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertIsNone(self._materialize(_ws(None), "events", payload)) + + # --- Camera-prefixed --- + + def test_restricted_role_sees_allowed_camera(self): + self.assertIsNotNone( + self._materialize(_ws("house_only"), "front_door/detect/state", "ON") + ) + + def test_restricted_role_blocked_from_unallowed_camera(self): + self.assertIsNone( + self._materialize(_ws("house_only"), "garage/detect/state", "ON") + ) + + def test_admin_sees_all_camera_topics(self): + self.assertIsNotNone( + self._materialize(_ws("admin"), "garage/detect/state", "ON") + ) + + # --- Unrestricted-only (zones, birdseye_layout) --- + + def test_zone_aggregate_blocked_for_restricted(self): + self.assertIsNone(self._materialize(_ws("house_only"), "driveway/person", 3)) + + def test_zone_aggregate_visible_to_admin(self): + self.assertIsNotNone(self._materialize(_ws("admin"), "driveway/person", 3)) + + def test_birdseye_layout_blocked_for_restricted(self): + payload = json.dumps( + {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}} + ) + self.assertIsNone( + self._materialize(_ws("house_only"), "birdseye_layout", payload) + ) + + def test_birdseye_layout_visible_to_admin(self): + payload = json.dumps( + {"front_door": {"x": 0, "y": 0, "width": 100, "height": 100}} + ) + self.assertIsNotNone( + self._materialize(_ws("admin"), "birdseye_layout", payload) + ) + + # --- Payload-camera --- + + def test_events_filtered_by_payload_camera(self): + payload = json.dumps({"after": {"camera": "garage"}}) + self.assertIsNone(self._materialize(_ws("house_only"), "events", payload)) + + payload = json.dumps({"after": {"camera": "front_door"}}) + self.assertIsNotNone(self._materialize(_ws("house_only"), "events", payload)) + + def test_events_with_missing_camera_dropped(self): + payload = json.dumps({"after": {}}) + self.assertIsNone(self._materialize(_ws("house_only"), "events", payload)) + + def test_triggers_filtered_by_payload_camera(self): + payload = json.dumps({"name": "t1", "camera": "garage"}) + self.assertIsNone(self._materialize(_ws("house_only"), "triggers", payload)) + + # --- Reshape: dict keyed by camera --- + + def test_camera_activity_filtered_to_allowed_keys(self): + payload = json.dumps( + { + "front_door": {"objects": 1}, + "back_door": {"objects": 0}, + "garage": {"objects": 2}, + } + ) + message = self._materialize(_ws("house_only"), "camera_activity", payload) + self.assertIsNotNone(message) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner.keys()), {"front_door", "back_door"}) + self.assertNotIn("garage", inner) + + def test_camera_activity_unchanged_for_admin(self): + payload = json.dumps({"front_door": {}, "back_door": {}, "garage": {}}) + message = self._materialize(_ws("admin"), "camera_activity", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_camera_activity_with_no_allowed_returns_none(self): + payload = json.dumps({"garage": {"objects": 2}}) + self.assertIsNone( + self._materialize(_ws("house_only"), "camera_activity", payload) + ) + + def test_audio_detections_filtered_to_allowed_keys(self): + payload = json.dumps({"front_door": {"bark": {}}, "garage": {"speech": {}}}) + message = self._materialize(_ws("house_only"), "audio_detections", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner.keys()), {"front_door"}) + + # --- Reshape: job_state --- + + def test_job_state_admin_sees_full_payload(self): + payload = json.dumps( + { + "motion_search": {"job_type": "motion_search", "camera": "garage"}, + "media_sync": {"job_type": "media_sync"}, + } + ) + message = self._materialize(_ws("admin"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_restricted_keeps_allowed_camera_jobs(self): + """Top-level camera field on a job entry: drop if not allowed.""" + payload = json.dumps( + { + "motion_search": {"job_type": "motion_search", "camera": "front_door"}, + "vlm_watch": {"job_type": "vlm_watch", "camera": "garage"}, + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("motion_search", inner) + self.assertNotIn("vlm_watch", inner) + + def test_job_state_export_results_jobs_filtered_per_recipient(self): + """The aggregated export broadcast nests per-camera sub-jobs under + ``results.jobs``. Restricted users must only see allowed entries.""" + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "front_door", "id": "a"}, + {"job_type": "export", "camera": "garage", "id": "b"}, + {"job_type": "export", "camera": "back_door", "id": "c"}, + ] + }, + } + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("export", inner) + kept_cameras = [j["camera"] for j in inner["export"]["results"]["jobs"]] + self.assertEqual(kept_cameras, ["front_door", "back_door"]) + # Sibling fields like ``status`` must survive reshaping. + self.assertEqual(inner["export"]["status"], "running") + + def test_job_state_export_entry_dropped_when_no_jobs_allowed(self): + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "garage", "id": "b"}, + ] + }, + } + } + ) + self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload)) + + # --- Reshape: stats --- + + def _stats_payload(self) -> str: + return json.dumps( + { + "cameras": { + "front_door": {"camera_fps": 5.0, "pid": 1234}, + "back_door": {"camera_fps": 5.0, "pid": 1235}, + "garage": {"camera_fps": 5.0, "pid": 1236}, + }, + "detectors": {"cpu": {"detection_start": 0.0, "inference_speed": 10}}, + "service": {"uptime": 12345, "version": "0.16.0"}, + "camera_fps": 15.0, + "detection_fps": 6.0, + } + ) + + def test_stats_admin_sees_full_payload(self): + message = self._materialize(_ws("admin"), "stats", self._stats_payload()) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], self._stats_payload()) + + def test_stats_restricted_filters_camera_keys_but_keeps_aggregates(self): + message = self._materialize(_ws("house_only"), "stats", self._stats_payload()) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(set(inner["cameras"].keys()), {"front_door", "back_door"}) + self.assertNotIn("garage", inner["cameras"]) + # Aggregates, detectors, and service block must survive. + self.assertEqual(inner["camera_fps"], 15.0) + self.assertEqual(inner["detection_fps"], 6.0) + self.assertIn("detectors", inner) + self.assertIn("service", inner) + + def test_stats_restricted_with_no_allowed_cameras_still_sends_aggregates(self): + """A restricted role whose allow-list contains only nonexistent cameras + still gets the global aggregates and service block.""" + config = _build_config(extra_roles={"empty_role": ["nonexistent"]}) + from frigate.comms.ws import _parse_json_payload + + payload = self._stats_payload() + all_cameras = set(config.cameras.keys()) + scope = _classify_outbound("stats", all_cameras, _collect_zone_names(config)) + full = json.dumps({"topic": "stats", "payload": payload}) + message = _materialize_for_ws( + _ws("empty_role"), + "stats", + full, + scope, + _parse_json_payload(payload), + config, + ) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertEqual(inner["cameras"], {}) + self.assertEqual(inner["camera_fps"], 15.0) + self.assertIn("service", inner) + + def test_stats_without_cameras_key_passes_through(self): + """A malformed stats payload missing the cameras sub-dict shouldn't + break delivery for restricted users — fall back to the full message.""" + payload = json.dumps({"detectors": {}, "service": {}, "detection_fps": 0.0}) + message = self._materialize(_ws("house_only"), "stats", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_export_entry_unchanged_for_admin(self): + payload = json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"job_type": "export", "camera": "garage", "id": "b"}, + ] + }, + } + } + ) + message = self._materialize(_ws("admin"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + self.assertEqual(envelope["payload"], payload) + + def test_job_state_restricted_keeps_global_jobs(self): + """media_sync has no camera field; restricted users still see it.""" + payload = json.dumps( + {"media_sync": {"job_type": "media_sync", "status": "running"}} + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("media_sync", inner) + + def test_job_state_debug_replay_nested_source_camera_filtered(self): + """debug_replay puts ``source_camera`` inside ``results`` (see + jobs/debug_replay.py:to_dict). Restricted users must not receive + entries whose nested source camera is unauthorized.""" + payload = json.dumps( + { + "debug_replay": { + "id": "bd6dc99d-a7d", + "job_type": "debug_replay", + "status": "running", + "start_time": 1.0, + "end_time": None, + "error_message": None, + "results": { + "current_step": "preparing_clip", + "progress_percent": 0.0, + "source_camera": "garage", + "replay_camera_name": "_replay_garage", + "start_ts": 0.0, + "end_ts": 1.0, + }, + } + } + ) + self.assertIsNone(self._materialize(_ws("house_only"), "job_state", payload)) + + def test_job_state_debug_replay_nested_source_camera_allowed(self): + payload = json.dumps( + { + "debug_replay": { + "id": "bd6dc99d-a7d", + "job_type": "debug_replay", + "status": "running", + "results": { + "source_camera": "front_door", + "replay_camera_name": "_replay_front_door", + }, + } + } + ) + message = self._materialize(_ws("house_only"), "job_state", payload) + envelope = json.loads(message) # type: ignore[arg-type] + inner = json.loads(envelope["payload"]) + self.assertIn("debug_replay", inner) + self.assertEqual( + inner["debug_replay"]["results"]["source_camera"], "front_door" + ) + + +class _FakeManager: + """Minimal ws4py manager: holds clients and exposes a lock.""" + + def __init__(self, clients: list[Any]) -> None: + self.lock = threading.Lock() + self.websockets = {id(c): c for c in clients} + + +class _FakeServer: + def __init__(self, manager: _FakeManager) -> None: + self.manager = manager + + +class _CapturingWs(SimpleNamespace): + """Fake ws4py client that records what was sent.""" + + def __init__(self, role: str | None) -> None: + environ = {} if role is None else {"HTTP_REMOTE_ROLE": role} + super().__init__(environ=environ, terminated=False) + self.sent: list[str] = [] + + def send(self, message: str) -> None: # noqa: D401 - matches ws4py API + self.sent.append(message) + + +class TestPublishEndToEnd(unittest.TestCase): + """Drive WebSocketClient.publish() against fake clients with different roles.""" + + def setUp(self): + self.config = _build_config( + extra_zones={"front_door": {"driveway": {"coordinates": "0,0,1,0,1,1,0,1"}}} + ) + self.admin = _CapturingWs("admin") + self.restricted = _CapturingWs("house_only") + self.anon = _CapturingWs(None) + self.client = WebSocketClient(self.config) + self.client.websocket_server = _FakeServer( + _FakeManager([self.admin, self.restricted, self.anon]) + ) + + def _payloads(self, ws: _CapturingWs) -> list[Any]: + return [json.loads(m)["payload"] for m in ws.sent] + + def test_global_topic_reaches_everyone(self): + self.client.publish("model_state", "{}") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 1) + self.assertEqual(len(self.anon.sent), 1) + + def test_camera_topic_filters_restricted_recipient(self): + self.client.publish("garage/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + self.assertEqual(len(self.anon.sent), 0) + + def test_camera_topic_allows_restricted_recipient_for_allowed_camera(self): + self.client.publish("front_door/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 1) + self.assertEqual(len(self.anon.sent), 0) + + def test_events_payload_filtered(self): + self.client.publish("events", json.dumps({"after": {"camera": "garage"}})) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + def test_camera_activity_reshaped_per_recipient(self): + self.client.publish( + "camera_activity", + json.dumps( + { + "front_door": {"objects": 1}, + "back_door": {"objects": 0}, + "garage": {"objects": 2}, + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual(set(admin_inner.keys()), {"front_door", "back_door", "garage"}) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual(set(restricted_inner.keys()), {"front_door", "back_door"}) + + self.assertEqual(len(self.anon.sent), 0) + + def test_birdseye_layout_blocked_for_restricted_and_anon(self): + self.client.publish( + "birdseye_layout", + json.dumps({"front_door": {"x": 0, "y": 0, "width": 1, "height": 1}}), + ) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + self.assertEqual(len(self.anon.sent), 0) + + def test_zone_aggregate_blocked_for_restricted(self): + self.client.publish("driveway/person", 2) + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + def test_stats_reshaped_per_recipient(self): + self.client.publish( + "stats", + json.dumps( + { + "cameras": { + "front_door": {"camera_fps": 5.0}, + "garage": {"camera_fps": 5.0}, + }, + "service": {"uptime": 1}, + "camera_fps": 10.0, + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual(set(admin_inner["cameras"].keys()), {"front_door", "garage"}) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual(set(restricted_inner["cameras"].keys()), {"front_door"}) + self.assertEqual(restricted_inner["camera_fps"], 10.0) + self.assertIn("service", restricted_inner) + + # Stats requires a role; anonymous gets nothing. + self.assertEqual(len(self.anon.sent), 0) + + def test_export_job_state_filters_results_jobs_per_recipient(self): + self.client.publish( + "job_state", + json.dumps( + { + "export": { + "job_type": "export", + "status": "running", + "results": { + "jobs": [ + {"camera": "front_door", "id": "a"}, + {"camera": "garage", "id": "b"}, + ] + }, + } + } + ), + ) + self.assertEqual(len(self.admin.sent), 1) + admin_inner = json.loads(self._payloads(self.admin)[0]) + self.assertEqual( + [j["camera"] for j in admin_inner["export"]["results"]["jobs"]], + ["front_door", "garage"], + ) + + self.assertEqual(len(self.restricted.sent), 1) + restricted_inner = json.loads(self._payloads(self.restricted)[0]) + self.assertEqual( + [j["camera"] for j in restricted_inner["export"]["results"]["jobs"]], + ["front_door"], + ) + + def test_unknown_topic_dropped_for_everyone(self): + self.client.publish("some_rogue_topic", "data") + self.assertEqual(self.admin.sent, []) + self.assertEqual(self.restricted.sent, []) + self.assertEqual(self.anon.sent, []) + + def test_terminated_client_is_skipped(self): + self.restricted.terminated = True + self.client.publish("front_door/detect/state", "ON") + self.assertEqual(len(self.admin.sent), 1) + self.assertEqual(len(self.restricted.sent), 0) + + +if __name__ == "__main__": + unittest.main() From 3f7768a48f5fc5b4a8dd6539b64e29d4515bc54e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:38 +0200 Subject: [PATCH 11/94] Translated using Weblate (Thai) Currently translated at 31.8% (7 of 22 strings) Translated using Weblate (Thai) Currently translated at 22.8% (40 of 175 strings) Translated using Weblate (Thai) Currently translated at 15.2% (9 of 59 strings) Translated using Weblate (Thai) Currently translated at 15.5% (7 of 45 strings) Translated using Weblate (Thai) Currently translated at 0.5% (4 of 794 strings) Translated using Weblate (Thai) Currently translated at 16.0% (4 of 25 strings) Translated using Weblate (Thai) Currently translated at 16.0% (8 of 50 strings) Translated using Weblate (Thai) Currently translated at 13.5% (8 of 59 strings) Translated using Weblate (Thai) Currently translated at 37.5% (24 of 64 strings) Translated using Weblate (Thai) Currently translated at 21.7% (38 of 175 strings) Translated using Weblate (Thai) Currently translated at 12.0% (3 of 25 strings) Translated using Weblate (Thai) Currently translated at 11.6% (10 of 86 strings) Translated using Weblate (Thai) Currently translated at 14.0% (7 of 50 strings) Translated using Weblate (Thai) Currently translated at 13.3% (6 of 45 strings) Translated using Weblate (Thai) Currently translated at 7.8% (90 of 1141 strings) Translated using Weblate (Thai) Currently translated at 0.3% (3 of 794 strings) Translated using Weblate (Thai) Currently translated at 4.6% (6 of 129 strings) Translated using Weblate (Thai) Currently translated at 1.2% (6 of 473 strings) Translated using Weblate (Thai) Currently translated at 77.6% (184 of 237 strings) Translated using Weblate (Thai) Currently translated at 9.6% (14 of 145 strings) Translated using Weblate (Thai) Currently translated at 27.2% (6 of 22 strings) Translated using Weblate (Thai) Currently translated at 8.0% (2 of 25 strings) Translated using Weblate (Thai) Currently translated at 11.8% (7 of 59 strings) Translated using Weblate (Thai) Currently translated at 8.8% (4 of 45 strings) Translated using Weblate (Thai) Currently translated at 10.4% (9 of 86 strings) Translated using Weblate (Thai) Currently translated at 16.0% (16 of 100 strings) Translated using Weblate (Thai) Currently translated at 21.1% (37 of 175 strings) Translated using Weblate (Thai) Currently translated at 15.0% (6 of 40 strings) Translated using Weblate (Thai) Currently translated at 45.0% (27 of 60 strings) Co-authored-by: Hosted Weblate Co-authored-by: Ton Zabretooth Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/th/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/th/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation Translation: Frigate NVR/common Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-classificationmodel Translation: Frigate NVR/views-events Translation: Frigate NVR/views-explore Translation: Frigate NVR/views-exports Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/th/common.json | 3 ++- web/public/locales/th/config/cameras.json | 17 ++++++++++++++++- web/public/locales/th/config/global.json | 17 ++++++++++++++++- web/public/locales/th/config/groups.json | 19 ++++++++++++++++++- web/public/locales/th/config/validation.json | 10 +++++++++- web/public/locales/th/views/chat.json | 11 ++++++++++- .../locales/th/views/classificationModel.json | 6 +++++- web/public/locales/th/views/events.json | 3 ++- web/public/locales/th/views/explore.json | 5 ++++- web/public/locales/th/views/exports.json | 6 ++++++ web/public/locales/th/views/faceLibrary.json | 8 +++++--- web/public/locales/th/views/live.json | 11 ++++++++++- web/public/locales/th/views/motionSearch.json | 12 +++++++++++- web/public/locales/th/views/replay.json | 14 +++++++++++++- web/public/locales/th/views/settings.json | 3 ++- web/public/locales/th/views/system.json | 12 +++++++++++- 16 files changed, 140 insertions(+), 17 deletions(-) diff --git a/web/public/locales/th/common.json b/web/public/locales/th/common.json index b920787973..75cbef6a07 100644 --- a/web/public/locales/th/common.json +++ b/web/public/locales/th/common.json @@ -66,7 +66,8 @@ "12hour": "MM-dd-yy-h-mm-ss-a", "24hour": "MM-dd-yy-HH-mm-ss" }, - "formattedTimestampMonthDay": "MMM d" + "formattedTimestampMonthDay": "MMM d", + "never": "ไม่เคย" }, "label": { "back": "ย้อนกลับ" diff --git a/web/public/locales/th/config/cameras.json b/web/public/locales/th/config/cameras.json index 0967ef424b..4a4191953a 100644 --- a/web/public/locales/th/config/cameras.json +++ b/web/public/locales/th/config/cameras.json @@ -1 +1,16 @@ -{} +{ + "label": "ตั้งค่ากล้อง", + "name": { + "label": "ชื่อกล้อง" + }, + "friendly_name": { + "label": "ชื่อแบบจำง่าย" + }, + "enabled": { + "label": "ถูกเปิดอยู่", + "description": "ถูกเปิดอยู่" + }, + "audio": { + "label": "การตรวจจับเสียง" + } +} diff --git a/web/public/locales/th/config/global.json b/web/public/locales/th/config/global.json index 0967ef424b..86743dce82 100644 --- a/web/public/locales/th/config/global.json +++ b/web/public/locales/th/config/global.json @@ -1 +1,16 @@ -{} +{ + "version": { + "label": "การตั้งค่าปัจจุบัน" + }, + "environment_vars": { + "label": "สภาพแวดล้อมที่หลากหลาย" + }, + "audio": { + "label": "การตรวจจับเสียง" + }, + "auth": { + "enabled": { + "label": "เปิดใช้การยืนยันตัวตน" + } + } +} diff --git a/web/public/locales/th/config/groups.json b/web/public/locales/th/config/groups.json index 0967ef424b..43ae3e5bc7 100644 --- a/web/public/locales/th/config/groups.json +++ b/web/public/locales/th/config/groups.json @@ -1 +1,18 @@ -{} +{ + "audio": { + "cameras": { + "detection": "การตรวจจับ", + "sensitivity": "ความอ่อนไหว" + } + }, + "snapshots": { + "cameras": { + "display": "แสดงผล" + } + }, + "detect": { + "cameras": { + "resolution": "ความละเอียด" + } + } +} diff --git a/web/public/locales/th/config/validation.json b/web/public/locales/th/config/validation.json index 0967ef424b..1e9b0d54d7 100644 --- a/web/public/locales/th/config/validation.json +++ b/web/public/locales/th/config/validation.json @@ -1 +1,9 @@ -{} +{ + "maximum": "มากที่สุดไม่เกิน {{limit}}", + "exclusiveMinimum": "ต้องเกินกว่า {{limit}}", + "exclusiveMaximum": "ต้องน้อยกว่า {{limit}}", + "minLength": "จำนวนอย่างน้อย {{limit}} อักขระ", + "maxLength": "ต้องไม่เกิน {{limit}} อักขระ", + "maxItems": "ต้องไม่เกิน {{limit}}", + "minimum": "ขั้นต่ำ {{limit}}" +} diff --git a/web/public/locales/th/views/chat.json b/web/public/locales/th/views/chat.json index 0967ef424b..4390d3961c 100644 --- a/web/public/locales/th/views/chat.json +++ b/web/public/locales/th/views/chat.json @@ -1 +1,10 @@ -{} +{ + "documentTitle": "สนทนา - Frigate", + "subtitle": "AI ผู้ช่วยบริหารจัดการข้อมูลเชิงลึกสำหรับกล้องวงจรปิดของคุณ", + "placeholder": "เชิญถาม…", + "error": "เกิดข้อขัดข้อง โปรดลองอีกครั้ง", + "processing": "กำลังประมวลผล…", + "showTools": "แสดงเครื่องมือ ({{count}})", + "response": "ตอบกลับ", + "attachment_chip_remove": "เอาสิ่งที่แนบออก" +} diff --git a/web/public/locales/th/views/classificationModel.json b/web/public/locales/th/views/classificationModel.json index 5d1307ccf7..81389c3328 100644 --- a/web/public/locales/th/views/classificationModel.json +++ b/web/public/locales/th/views/classificationModel.json @@ -2,9 +2,13 @@ "documentTitle": "โมเดลการจำแนกประเภท- Frigate", "details": { "scoreInfo": "คะแนน (Score) คือค่าเฉลี่ยของความมั่นใจในการจำแนกประเภท (Classification Confidence) จากการตรวจจับวัตถุชิ้นนี้ในทุกๆ ครั้ง", - "none": "ไม่มี" + "none": "ไม่มี", + "unknown": "ไม่ทราบ" }, "description": { "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น" + }, + "button": { + "deleteImages": "ลบภาพ" } } diff --git a/web/public/locales/th/views/events.json b/web/public/locales/th/views/events.json index f303ea6b3c..9279e9ae33 100644 --- a/web/public/locales/th/views/events.json +++ b/web/public/locales/th/views/events.json @@ -34,5 +34,6 @@ "detections": "การตรวจจับ", "selected_one": "เลือก {{count}} แล้ว", "timeline.aria": "เลือกไทม์ไลน์", - "documentTitle": "รีวิว - Frigate" + "documentTitle": "รีวิว - Frigate", + "zoomIn": "ซูมเข้า" } diff --git a/web/public/locales/th/views/explore.json b/web/public/locales/th/views/explore.json index 030d228994..8c17c33b03 100644 --- a/web/public/locales/th/views/explore.json +++ b/web/public/locales/th/views/explore.json @@ -8,7 +8,10 @@ "context": "สํารวจสามารถใช้หลังจากติดตามวัตถุเสร็จ.", "startingUp": "เริ่มต้น…", "estimatedTime": "ระยะเวลาโดยประมาณ:", - "finishingShortly": "เสร็จเร็วๆนี้" + "finishingShortly": "เสร็จเร็วๆนี้", + "step": { + "thumbnailsEmbedded": "รูปภาพย่อที่ฝังไว้: " + } }, "downloadingModels": { "tips": { diff --git a/web/public/locales/th/views/exports.json b/web/public/locales/th/views/exports.json index 698c6f82b8..1c58da8353 100644 --- a/web/public/locales/th/views/exports.json +++ b/web/public/locales/th/views/exports.json @@ -13,5 +13,11 @@ "error": { "renameExportFailed": "ผิดพลาดในการแก้ไขชื่อการส่งออก: {{errorMessage}}" } + }, + "headings": { + "cases": "กรณี" + }, + "tooltip": { + "editName": "เปลี่ยนชื่อ" } } diff --git a/web/public/locales/th/views/faceLibrary.json b/web/public/locales/th/views/faceLibrary.json index d663a7bcf6..4cbefcc7ab 100644 --- a/web/public/locales/th/views/faceLibrary.json +++ b/web/public/locales/th/views/faceLibrary.json @@ -2,7 +2,7 @@ "details": { "person": "คน", "subLabelScore": "คะแนน Sub Label", - "unknown": "ไม่รู้", + "unknown": "ไม่ทราบ", "timestamp": "เวลา" }, "steps": { @@ -46,7 +46,8 @@ "description": { "addFace": "เพิ่มคอลเลกชันใหม่ไปยังคลังใบหน้า โดยการอัปโหลดรูปภาพแรก", "placeholder": "ใส่ชื่อสําหรับคอลเลกชันนี้", - "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น" + "invalidName": "ชื่อไม่ถูกต้อง ชื่อสามารถประกอบได้ด้วยตัวอักษร, ตัวเลข, ช่องว่าง, เครื่องหมาย ( ' , _ , - ) เท่านั้น", + "nameCannotContainHash": "ชื่อ ห้ามมีเครื่องหมาย #" }, "toast": { "success": { @@ -54,5 +55,6 @@ "deletedName_other": "{{count}} หน้าถูกลบไปเรียบร้อยแล้ว." } }, - "readTheDocs": "อ่านเอกสาร" + "readTheDocs": "อ่านเอกสาร", + "documentTitle": "คลังข้อมูลใบหน้า - Frigate" } diff --git a/web/public/locales/th/views/live.json b/web/public/locales/th/views/live.json index ccf620b84c..3bdd408f89 100644 --- a/web/public/locales/th/views/live.json +++ b/web/public/locales/th/views/live.json @@ -12,7 +12,9 @@ "tips.documentation": "อ่านเอกสาร " } }, - "documentTitle": "สด - Frigate", + "documentTitle": { + "default": "ถ่ายทอดสด - Frigate" + }, "lowBandwidthMode": "โหมดแบนด์วิดท์ต่ำ", "twoWayTalk": { "enable": "เปิดใช้งานการสนทนาสองทาง", @@ -45,5 +47,12 @@ }, "recording": { "disable": "ปิดการบันทึก" + }, + "ptz": { + "move": { + "clickMove": { + "label": "คลิกที่ภาพ เพื่อเลือกตำแหน่งที่จะตั้งค่าให้เป็นศูนย์กลางภาพ" + } + } } } diff --git a/web/public/locales/th/views/motionSearch.json b/web/public/locales/th/views/motionSearch.json index 0967ef424b..852fb09cb9 100644 --- a/web/public/locales/th/views/motionSearch.json +++ b/web/public/locales/th/views/motionSearch.json @@ -1 +1,11 @@ -{} +{ + "documentTitle": "ค้นหาการเคลื่อนไหว - Frigate", + "title": "ค้นหาการเคลื่อนไหว", + "description": "วาดเส้นกรอบกำหนดขอบเขตที่ต้องการ และระบุช่วงเวลาค้นหาการเคลื่อนไหวในบริเวณนั้น", + "startSearch": "เริ่มการค้นหา", + "searchStarted": "การค้นหาเริ่มแล้ว", + "searchCancelled": "เลิกค้นหาแล้ว", + "cancelSearch": "ยกเลิก", + "noChangesFound": "ไม่มีการเปลี่ยนแปลงในภาพบริเวณที่เลือก", + "jumpToTime": "ข้ามมาที่เวลานี้" +} diff --git a/web/public/locales/th/views/replay.json b/web/public/locales/th/views/replay.json index 0967ef424b..1c8cc9b831 100644 --- a/web/public/locales/th/views/replay.json +++ b/web/public/locales/th/views/replay.json @@ -1 +1,13 @@ -{} +{ + "websocket_messages": "ข้อความ", + "dialog": { + "camera": "ภาพจากกล้อง", + "timeRange": "ช่วงเวลา", + "preset": { + "1m": "1 นาทีสุดท้าย" + }, + "startButton": "เริ่มเล่นภาพย้อนหลัง", + "selectFromTimeline": "เลือก", + "startLabel": "เริ่ม" + } +} diff --git a/web/public/locales/th/views/settings.json b/web/public/locales/th/views/settings.json index b848a4e279..ebd2805da4 100644 --- a/web/public/locales/th/views/settings.json +++ b/web/public/locales/th/views/settings.json @@ -109,7 +109,8 @@ "cameraManagement": "จัดการกล้อง - Frigate", "enrichments": "การตั้งค่าของเพิ่มเติม - Frigate", "motionTuner": "ปรับแต่งการเคลื่อนไหว - Frigate", - "object": "ดีบั๊ก - Frigate" + "object": "ดีบั๊ก - Frigate", + "cameraReview": "แสดงการตั้งค่าของกล้อง - Frigate" }, "menu": { "notifications": "การแจ้งเตือน", diff --git a/web/public/locales/th/views/system.json b/web/public/locales/th/views/system.json index 4ab0f7361f..90e29a25ef 100644 --- a/web/public/locales/th/views/system.json +++ b/web/public/locales/th/views/system.json @@ -64,7 +64,17 @@ "logs": { "frigate": "Frigate Logs - Frigate", "go2rtc": "Logs ของ Go2RTC - Frigate", - "nginx": "Logs ของ Nginx - Frigate" + "nginx": "Logs ของ Nginx - Frigate", + "websocket": "ประวัติข้อความ - Frigate" + } + }, + "logs": { + "websocket": { + "pause": "พัก", + "clear": "ลบล้าง", + "filter": { + "all": "ทุกหัวข้อ" + } } } } From d439b09f90b17907894182a6bfdb82b0c449c89b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:40 +0200 Subject: [PATCH 12/94] Translated using Weblate (German) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (German) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (German) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (German) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (German) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (German) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (German) Currently translated at 100.0% (50 of 50 strings) Co-authored-by: Hosted Weblate Co-authored-by: Sebastian Sie Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/de/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-settings --- web/public/locales/de/common.json | 3 +- web/public/locales/de/config/cameras.json | 6 +- web/public/locales/de/config/global.json | 6 +- web/public/locales/de/objects.json | 6 +- web/public/locales/de/views/chat.json | 18 ++++ web/public/locales/de/views/faceLibrary.json | 6 +- web/public/locales/de/views/settings.json | 89 ++++++++++++++++++-- 7 files changed, 124 insertions(+), 10 deletions(-) diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json index 7f9848fe28..c1ed6020b8 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -192,7 +192,8 @@ "bg": "Български (bulgarisch)", "gl": "Galego (Galicisch)", "id": "Bahasa Indonesia (Indonesisch)", - "hr": "Hrvatski (Kroatisch)" + "hr": "Hrvatski (Kroatisch)", + "bs": "Bosnisch" }, "appearance": "Erscheinung", "theme": { diff --git a/web/public/locales/de/config/cameras.json b/web/public/locales/de/config/cameras.json index 7080fe186a..126295932d 100644 --- a/web/public/locales/de/config/cameras.json +++ b/web/public/locales/de/config/cameras.json @@ -25,7 +25,11 @@ }, "filters": { "label": "Audiofilter", - "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden." + "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.", + "threshold": { + "label": "Mindestvertrauensgrad für Audio", + "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird." + } }, "max_not_heard": { "label": "Ende Timeout", diff --git a/web/public/locales/de/config/global.json b/web/public/locales/de/config/global.json index 9df4d44c8a..fb319cdecf 100644 --- a/web/public/locales/de/config/global.json +++ b/web/public/locales/de/config/global.json @@ -23,7 +23,11 @@ }, "filters": { "label": "Audiofilter", - "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden." + "description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden.", + "threshold": { + "label": "Mindestvertrauensgrad für Audio", + "description": "Mindestschwellenwert für die Zuverlässigkeit, damit das Audioereignis gezählt wird." + } }, "max_not_heard": { "label": "Ende Timeout", diff --git a/web/public/locales/de/objects.json b/web/public/locales/de/objects.json index ae767c61db..4380ef181e 100644 --- a/web/public/locales/de/objects.json +++ b/web/public/locales/de/objects.json @@ -121,5 +121,9 @@ "royal_mail": "Royal-Mail", "school_bus": "Schulbus", "skunk": "Stinktier", - "kangaroo": "Känguruh" + "kangaroo": "Känguruh", + "baby": "Baby", + "baby_stroller": "Kinderwagen", + "rickshaw": "Rikscha", + "rodent": "Nagetier" } diff --git a/web/public/locales/de/views/chat.json b/web/public/locales/de/views/chat.json index 5a87ce9e10..7c66676013 100644 --- a/web/public/locales/de/views/chat.json +++ b/web/public/locales/de/views/chat.json @@ -42,5 +42,23 @@ "show_camera_status": "Wie ist der aktuelle Status meiner Kameras?", "recap": "Was ist passiert, während ich weg war?", "watch_camera": "Pass auf die Haustür auf und sag mir Bescheid, wenn jemand kommt" + }, + "new_chat": "Neuer Chat", + "settings": { + "title": "Chat Einstellung", + "show_stats": { + "title": "Statistiken anzeigen", + "desc": "Generierungsrate und Kontextgröße für Chat-Antworten anzeigen.", + "while_generating": "Während der Erstellung", + "always": "Immer" + }, + "auto_scroll": { + "title": "Auto scrollen", + "desc": "Verfolgen Sie neue Nachrichten, sobald sie eintreffen." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" } } diff --git a/web/public/locales/de/views/faceLibrary.json b/web/public/locales/de/views/faceLibrary.json index d9269fd0ee..7ece861a78 100644 --- a/web/public/locales/de/views/faceLibrary.json +++ b/web/public/locales/de/views/faceLibrary.json @@ -48,7 +48,11 @@ "title": "Neueste Erkennungen", "aria": "Wähle aktuelle Erkennungen", "empty": "Es gibt keine aktuellen Versuche zur Gesichtserkennung", - "titleShort": "frisch" + "titleShort": "frisch", + "emptyNoLibrary": { + "title": "Gesicht hinzufügen", + "description": "Sie müssen mindestens ein Gesicht zur Bibliothek hinzufügen, damit die Gesichtserkennung funktioniert." + } }, "deleteFaceLibrary": { "title": "Lösche Name", diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json index 6193333491..5c4028eadf 100644 --- a/web/public/locales/de/views/settings.json +++ b/web/public/locales/de/views/settings.json @@ -803,7 +803,15 @@ "availableModels": "Verfügbare Modelle", "loadingAvailableModels": "Lade verfügbare Modelle…", "baseModel": "Basis Model", - "title": "Model Informationen" + "title": "Model Informationen", + "noModelLoaded": "Derzeit ist kein „Frigate+“-Modell geladen.", + "selectModel": "Wählen Sie ein Modell aus", + "noModelsAvailable": "Keine Modelle verfügbar", + "filter": { + "ariaLabel": "Modelle nach Typ filtern", + "baseModels": "Basismodelle", + "fineTunedModels": "Optimierte Modelle" + } }, "toast": { "error": "Speichern der Konfigurationsänderungen fehlgeschlagen: {{errorMessage}}", @@ -1415,7 +1423,8 @@ "normal": "Normal", "dedicatedLpr": "Spezielles LPR-System", "saveSuccess": "Der Kameratyp für {{cameraName}} wurde aktualisiert. Starte Frigate neu, um die Änderungen zu übernehmen." - } + }, + "description": "Fügen Sie Kameras hinzu, bearbeiten und löschen Sie sie, legen Sie fest, welche Kameras aktiviert sind, und konfigurieren Sie profil- und kameratypabhängige Übersteuerungen. Um Streams, Erkennung, Bewegung und andere kameraspezifische Einstellungen zu konfigurieren, wählen Sie den entsprechenden Abschnitt unter „Kamerakonfiguration“ aus." }, "cameraReview": { "title": "Kamera-Einstellungen überprüfen", @@ -1489,7 +1498,13 @@ "othersField_one": "{{count}} andere", "othersField_other": "{{count}} weitere", "profilePrefix": "{{profile}} Profile: {{fields}}" - } + }, + "overriddenGlobalHeading_one": "Diese Kamera überschreibt das Feld {{count}} aus der globalen Konfiguration:", + "overriddenGlobalHeading_other": "Diese Kamera überschreibt alle Felder {{count}} aus der globalen Konfiguration:", + "overriddenGlobalNoDeltas": "Diese Kamera überschreibt die globale Konfiguration, es gibt jedoch keine Abweichungen bei den Feldwerten.", + "overriddenBaseConfigHeading_one": "Das Profil {{profile}} überschreibt das Feld {{count}} aus der Basiskonfiguration:", + "overriddenBaseConfigHeading_other": "Das Profil {{profile}} überschreibt di Felder {{count}} aus der Basiskonfiguration:", + "overriddenBaseConfigNoDeltas": "Das Profil {{profile}} überschreibt diesen Abschnitt, jedoch weichen keine Feldwerte von der Basiskonfiguration ab." }, "timestampPosition": { "tl": "Oben links", @@ -1726,7 +1741,9 @@ "options": { "embeddings": "Einbetten", "vision": "Vision", - "tools": "Werkzeuge" + "tools": "Werkzeuge", + "descriptions": "Beschreibung", + "chat": "Chat" } }, "semanticSearchModel": { @@ -1884,7 +1901,14 @@ }, "onvif": { "profileAuto": "Auto", - "profileLoading": "Profile werden geladen..." + "profileLoading": "Profile werden geladen...", + "autotracking": { + "zooming": { + "disabled": "deaktiviert", + "absolute": "Absolut", + "relative": "Verwandter" + } + } }, "configMessages": { "review": { @@ -1932,5 +1956,60 @@ "semanticSearch": { "jinav2SmallModelSize": "Die „kleine“ Variante des Jina V2-Modells verursacht hohe RAM- und Inferenzkosten. Es wird das „große“ Modell mit einer dedizierten GPU empfohlen." } + }, + "birdseye": { + "trackingMode": { + "objects": "Objekte", + "motion": "Bewegung", + "continuous": "Fortlaufend" + } + }, + "retainMode": { + "all": "Alle", + "motion": "Bewegung", + "active_objects": "Aktive Objekte" + }, + "previewQuality": { + "very_high": "sehr hoch", + "high": "hoch", + "medium": "Mittel", + "low": "niedrig", + "very_low": "sehr niedrig" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 Stunden", + "24hour": "24 Stunden" + }, + "TimeOrDateStyle": { + "full": "vollständig", + "long": "lang", + "medium": "mittel", + "short": "kurz" + }, + "unitSystem": { + "metric": "Metrik", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Aufnahmen", + "previews": "Vorschau" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Warnung", + "error": "Fehler", + "critical": "Kritisch" + } + }, + "modelSize": { + "small": "klein", + "large": "groß" } } From 6e5d55ff64e6e236ccfd90a26efa1f586bd2489d Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:41 +0200 Subject: [PATCH 13/94] Translated using Weblate (Estonian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Estonian) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Estonian) Currently translated at 100.0% (127 of 127 strings) Co-authored-by: Hosted Weblate Co-authored-by: Priit Jõerüüt Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/et/ Translation: Frigate NVR/common Translation: Frigate NVR/objects --- web/public/locales/et/common.json | 3 ++- web/public/locales/et/objects.json | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/public/locales/et/common.json b/web/public/locales/et/common.json index 4455b56973..3a6a82008f 100644 --- a/web/public/locales/et/common.json +++ b/web/public/locales/et/common.json @@ -140,7 +140,8 @@ "gl": "Galego (galeegi keel)", "id": "Bahasa Indonesia (indoneesia keel)", "ur": "اردو (urdu keel)", - "hr": "Hrvatski (horvaadi keel)" + "hr": "Hrvatski (horvaadi keel)", + "bs": "Bosanski (bosnia keel)" }, "system": "Süsteem", "systemMetrics": "Süsteemi meetrika", diff --git a/web/public/locales/et/objects.json b/web/public/locales/et/objects.json index 5cd7398b39..2e7e6c5aa5 100644 --- a/web/public/locales/et/objects.json +++ b/web/public/locales/et/objects.json @@ -121,5 +121,10 @@ "royal_mail": "Royal Mail", "school_bus": "Koolibuss", "skunk": "Vinukloom (skunk)", - "kangaroo": "Känguru" + "kangaroo": "Känguru", + "baby": "Väikelaps", + "baby_stroller": "Lapsevanker", + "rickshaw": "Rikša", + "Rodent": "Näriline", + "rodent": "Näriline" } From dc2c48f6d7c62ac57e607cc654d58c7814e2d40f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:43 +0200 Subject: [PATCH 14/94] Translated using Weblate (Russian) Currently translated at 92.0% (23 of 25 strings) Translated using Weblate (Russian) Currently translated at 100.0% (22 of 22 strings) Co-authored-by: Hosted Weblate Co-authored-by: Max Slotov Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ru/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ru/ Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation --- web/public/locales/ru/config/groups.json | 3 ++- web/public/locales/ru/config/validation.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/web/public/locales/ru/config/groups.json b/web/public/locales/ru/config/groups.json index d69c495829..a7c9152f5b 100644 --- a/web/public/locales/ru/config/groups.json +++ b/web/public/locales/ru/config/groups.json @@ -1,7 +1,8 @@ { "audio": { "global": { - "sensitivity": "Общая чувствительность" + "sensitivity": "Общая чувствительность", + "detection": "Общее обнаружение" }, "cameras": { "detection": "Обнаружение", diff --git a/web/public/locales/ru/config/validation.json b/web/public/locales/ru/config/validation.json index 1d16abf7a4..2314881ed9 100644 --- a/web/public/locales/ru/config/validation.json +++ b/web/public/locales/ru/config/validation.json @@ -27,5 +27,6 @@ "detectRequired": "Как минимум один входной поток должен быть назначен роли 'detect'.", "hwaccelDetectOnly": "Только входной поток с ролью detect может настраивать аппаратное ускорение." } - } + }, + "minimum": "Должно быть минимум {{limit}}" } From 5ddf8bc1b08f5da9a5de0e05d1fc50be9c027baa Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:45 +0200 Subject: [PATCH 15/94] Translated using Weblate (Romanian) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (1129 of 1129 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Romanian) Currently translated at 100.0% (127 of 127 strings) Co-authored-by: Hosted Weblate Co-authored-by: lukasig Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ro/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ro/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ro/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ro/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-settings --- web/public/locales/ro/common.json | 3 +- web/public/locales/ro/config/cameras.json | 6 +- web/public/locales/ro/config/global.json | 6 +- web/public/locales/ro/objects.json | 7 +- web/public/locales/ro/views/faceLibrary.json | 6 +- web/public/locales/ro/views/settings.json | 98 +++++++++++++++++++- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/web/public/locales/ro/common.json b/web/public/locales/ro/common.json index 00716ac2c4..57a0262d6f 100644 --- a/web/public/locales/ro/common.json +++ b/web/public/locales/ro/common.json @@ -136,7 +136,8 @@ "gl": "Galego (Galiciană)", "id": "Bahasa Indonesia (Indoneziană)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Croată)" + "hr": "Hrvatski (Croată)", + "bs": "Bosanski (Bosniacă)" }, "theme": { "default": "Implicit", diff --git a/web/public/locales/ro/config/cameras.json b/web/public/locales/ro/config/cameras.json index 918598b0ad..f793ac9b1b 100644 --- a/web/public/locales/ro/config/cameras.json +++ b/web/public/locales/ro/config/cameras.json @@ -33,7 +33,11 @@ }, "filters": { "label": "Filtre audio", - "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere." + "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.", + "threshold": { + "label": "Încredere audio minimă", + "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare." + } }, "enabled_in_config": { "label": "Stare audio originală", diff --git a/web/public/locales/ro/config/global.json b/web/public/locales/ro/config/global.json index f7207df758..fff53a0778 100644 --- a/web/public/locales/ro/config/global.json +++ b/web/public/locales/ro/config/global.json @@ -19,7 +19,11 @@ }, "filters": { "label": "Filtre audio", - "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere." + "description": "Setări de filtrare per tip audio, cum ar fi pragul de încredere.", + "threshold": { + "label": "Încredere audio minimă", + "description": "Pragul minim de încredere pentru ca evenimentul audio să fie luat în considerare." + } }, "enabled_in_config": { "label": "Stare audio originală", diff --git a/web/public/locales/ro/objects.json b/web/public/locales/ro/objects.json index 90dfc34cb7..122244d5c4 100644 --- a/web/public/locales/ro/objects.json +++ b/web/public/locales/ro/objects.json @@ -121,5 +121,10 @@ "royal_mail": "Royal Mail", "school_bus": "Autobus Scolar", "skunk": "Sconcs", - "kangaroo": "Cangur" + "kangaroo": "Cangur", + "baby": "Bebeluș", + "baby_stroller": "Cărucior de copii", + "rickshaw": "Ricșă", + "Rodent": "Rozătoare", + "rodent": "Rozătoare" } diff --git a/web/public/locales/ro/views/faceLibrary.json b/web/public/locales/ro/views/faceLibrary.json index 15979a6c7a..a6227cdc6b 100644 --- a/web/public/locales/ro/views/faceLibrary.json +++ b/web/public/locales/ro/views/faceLibrary.json @@ -30,7 +30,11 @@ "empty": "Nu există încercări recente de recunoaștere facială", "title": "Recunoașteri Recente", "aria": "Selectează Recunoașteri Recente", - "titleShort": "Recent" + "titleShort": "Recent", + "emptyNoLibrary": { + "title": "Încarcă o față", + "description": "Trebuie să adaugi cel puțin o față în librărie pentru ca recunoașterea facială să funcționeze." + } }, "steps": { "description": { diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json index 4babba1d9e..f3636c14bb 100644 --- a/web/public/locales/ro/views/settings.json +++ b/web/public/locales/ro/views/settings.json @@ -781,7 +781,15 @@ "availableModels": "Modele Disponibile", "modelType": "Tip Model", "trainDate": "Data Antrenării", - "cameras": "Camere" + "cameras": "Camere", + "noModelLoaded": "Niciun model Frigate+ nu este încărcat în prezent.", + "selectModel": "Selectează un model", + "noModelsAvailable": "Niciun model disponibil", + "filter": { + "ariaLabel": "Filtrează modelele după tip", + "baseModels": "Modele de bază", + "fineTunedModels": "Modele optimizate" + } }, "toast": { "error": "Eroare la salvarea modificărilor de config: {{errorMessage}}", @@ -1364,7 +1372,8 @@ "normal": "Normal", "dedicatedLpr": "LPR dedicat", "saveSuccess": "Tipul camerei a fost actualizat pentru {{cameraName}}. Repornește Frigate pentru a aplica modificările." - } + }, + "description": "Adaugă, editează și șterge camere, controlează care camere sunt activate și configurează suprascrieri per profil și tip de cameră. Pentru a configura stream-uri, detecția, mișcarea și alte setări specifice camerei, alege secțiunea specifică din Configurare Cameră." }, "cameraReview": { "title": "Setări Review Cameră", @@ -1663,7 +1672,9 @@ "options": { "embeddings": "Înglobare", "vision": "Viziune", - "tools": "Instrumente" + "tools": "Instrumente", + "descriptions": "Descrieri", + "chat": "Chat" } }, "semanticSearchModel": { @@ -1745,7 +1756,15 @@ "othersField_few": "{{count}} alte", "othersField_other": "{{count}} de alte", "profilePrefix": "Profil {{profile}}: {{fields}}" - } + }, + "overriddenGlobalHeading_one": "Această cameră suprascrie {{count}} câmp din configurația globală:", + "overriddenGlobalHeading_few": "Această cameră suprascrie {{count}} câmpuri din configurația globală:", + "overriddenGlobalHeading_other": "Această cameră suprascrie {{count}} de câmpuri din configurația globală:", + "overriddenGlobalNoDeltas": "Această cameră suprascrie configurația globală, dar nicio valoare a câmpurilor nu diferă.", + "overriddenBaseConfigHeading_one": "Profilul {{profile}} suprascrie {{count}} câmp din configurația de bază:", + "overriddenBaseConfigHeading_few": "Profilul {{profile}} suprascrie {{count}} câmpuri din configurația de bază:", + "overriddenBaseConfigHeading_other": "Profilul {{profile}} suprascrie {{count}} de câmpuri din configurația de bază:", + "overriddenBaseConfigNoDeltas": "Profilul {{profile}} suprascrie această secțiune, dar nicio valoare a câmpurilor nu diferă de configurația de bază." }, "profiles": { "title": "Profile", @@ -1840,7 +1859,14 @@ }, "onvif": { "profileAuto": "Auto", - "profileLoading": "Se încarcă profilurile..." + "profileLoading": "Se încarcă profilurile...", + "autotracking": { + "zooming": { + "disabled": "Dezactivat", + "absolute": "Absolut", + "relative": "Relativ" + } + } }, "configMessages": { "review": { @@ -1888,5 +1914,67 @@ "semanticSearch": { "jinav2SmallModelSize": "Dimensiunea 'small' cu modelul Jina V2 are un cost ridicat de RAM și inferență. Modelul 'large' cu un GPU dedicat este recomandat." } + }, + "birdseye": { + "trackingMode": { + "objects": "Obiecte", + "motion": "Mișcare", + "continuous": "Continuu" + } + }, + "snapshot": { + "retainMode": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + } + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 ore", + "24hour": "24 de ore" + }, + "TimeOrDateStyle": { + "long": "Lung", + "medium": "Mediu", + "short": "Scurt", + "full": "Complet" + }, + "unitSystem": { + "metric": "Metric", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Înregistrări", + "previews": "Previzualizări" + } + }, + "logger": { + "logLevel": { + "debug": "Depanare", + "info": "Informații", + "warning": "Avertisment", + "error": "Eroare", + "critical": "Critic" + } + }, + "modelSize": { + "small": "Mic", + "large": "Mare" + }, + "retainMode": { + "all": "Toate", + "motion": "Mișcare", + "active_objects": "Obiecte active" + }, + "previewQuality": { + "medium": "Mediu", + "very_high": "Foarte ridicat", + "high": "Ridicat", + "low": "Scăzut", + "very_low": "Foarte scăzut" } } From 161f56b5d4e2ad888e19ca42c5ba107cfef16b9f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:47 +0200 Subject: [PATCH 16/94] Translated using Weblate (Catalan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (53 of 53 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (811 of 811 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1171 of 1171 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1162 of 1162 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1151 of 1151 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1150 of 1150 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (501 of 501 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1137 of 1137 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1129 of 1129 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (1122 of 1122 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Catalan) Currently translated at 100.0% (127 of 127 strings) Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com> Co-authored-by: Gerard Ricart Castells Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ca/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/audio Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-settings --- web/public/locales/ca/audio.json | 2 +- web/public/locales/ca/common.json | 3 +- web/public/locales/ca/config/cameras.json | 6 +- web/public/locales/ca/config/global.json | 43 ++++- web/public/locales/ca/objects.json | 7 +- web/public/locales/ca/views/chat.json | 23 +++ web/public/locales/ca/views/faceLibrary.json | 6 +- web/public/locales/ca/views/settings.json | 173 +++++++++++++++++-- 8 files changed, 241 insertions(+), 22 deletions(-) diff --git a/web/public/locales/ca/audio.json b/web/public/locales/ca/audio.json index 98ed63bb40..0cd1959b6c 100644 --- a/web/public/locales/ca/audio.json +++ b/web/public/locales/ca/audio.json @@ -138,7 +138,7 @@ "plucked_string_instrument": "Instrument de corda pinçada", "guitar": "Guitarra", "electric_guitar": "Guitarra elèctrica", - "bass_guitar": "Baix", + "bass_guitar": "Guitarra baixa", "acoustic_guitar": "Guitarra acústica", "steel_guitar": "Guitarra steel", "tapping": "Tapping", diff --git a/web/public/locales/ca/common.json b/web/public/locales/ca/common.json index a712459c3e..f089d62eb7 100644 --- a/web/public/locales/ca/common.json +++ b/web/public/locales/ca/common.json @@ -49,7 +49,8 @@ "gl": "Galego (Gallec)", "id": "Bahasa Indonesia (Indonesi)", "ur": "اردو (Urdú)", - "hr": "Hrvatski (croat)" + "hr": "Hrvatski (croat)", + "bs": "Bosanski (Bosni)" }, "system": "Sistema", "systemMetrics": "Mètriques del sistema", diff --git a/web/public/locales/ca/config/cameras.json b/web/public/locales/ca/config/cameras.json index 433bcf5ff6..26016deee0 100644 --- a/web/public/locales/ca/config/cameras.json +++ b/web/public/locales/ca/config/cameras.json @@ -33,7 +33,11 @@ }, "filters": { "label": "Filtres d'àudio", - "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius." + "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius.", + "threshold": { + "label": "Confiança mínima de l'àudio", + "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar." + } }, "enabled_in_config": { "label": "Estat d'àudio original", diff --git a/web/public/locales/ca/config/global.json b/web/public/locales/ca/config/global.json index 693e8c2840..f748860668 100644 --- a/web/public/locales/ca/config/global.json +++ b/web/public/locales/ca/config/global.json @@ -258,6 +258,41 @@ }, "raw_mask": { "label": "Màscara en brut" + }, + "filters_attribute": { + "label": "Filtres d'atribut", + "description": "Filtres aplicats als atributs detectats per reduir falsos positius (àrea, relació, confiança).", + "min_area": { + "label": "Àrea mínima de l'atribut", + "description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)." + }, + "max_area": { + "label": "Àrea màxima de l'atribut", + "description": "Es permet l'àrea màxima del contenidor (píxels o percentatge) per a aquest atribut. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)." + }, + "min_ratio": { + "label": "Relació mínima d'aspecte", + "description": "Relació mínima d'amplada/alçada requerida per a la casella contenidora a qualificar." + }, + "max_ratio": { + "label": "Relació màxima d'aspecte", + "description": "Es permet la relació màxima d'amplada/alçada per a la casella contenidora a qualificar." + }, + "threshold": { + "label": "Llindar de confiança", + "description": "Es requereix un llindar de confiança mitjà per a la detecció perquè l'atribut es consideri un veritable positiu." + }, + "min_score": { + "label": "Confiança mínima", + "description": "Es requereix una confiança mínima de detecció d'un sol fotograma per a associar aquest atribut amb el seu objecte pare." + }, + "mask": { + "label": "Màscara de filtre", + "description": "Coordenades de polígon que defineixen on s'aplica aquest filtre dins del marc." + }, + "raw_mask": { + "label": "Màscara en brut" + } } }, "record": { @@ -1987,7 +2022,11 @@ }, "filters": { "label": "Filtres d'àudio", - "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius." + "description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius.", + "threshold": { + "label": "Confiança mínima de l'àudio", + "description": "Llindar mínim de confiança per a l'esdeveniment d'àudio a comptar." + } }, "enabled_in_config": { "label": "Estat d'àudio original", @@ -2207,7 +2246,7 @@ }, "match_distance": { "label": "Distància de la coincidència", - "description": "Nombre de desajustos de caràcters permesos quan es comparen les plaques detectades amb les plaques conegudes." + "description": "Nombre de discrepàncies de caràcters permesos en comparar les plaques detectades amb les plaques conegudes." }, "known_plates": { "label": "Matricules conegudes", diff --git a/web/public/locales/ca/objects.json b/web/public/locales/ca/objects.json index 456f522ab0..17378dfe09 100644 --- a/web/public/locales/ca/objects.json +++ b/web/public/locales/ca/objects.json @@ -121,5 +121,10 @@ "royal_mail": "Royal Mail", "school_bus": "Bus escolar", "skunk": "Mofeta", - "kangaroo": "Cangur" + "kangaroo": "Cangur", + "baby": "Nadó", + "baby_stroller": "Cotxet", + "rickshaw": "Ricksaw", + "Rodent": "Rosegador", + "rodent": "Rosegador" } diff --git a/web/public/locales/ca/views/chat.json b/web/public/locales/ca/views/chat.json index 064c0d81bf..27a2cce825 100644 --- a/web/public/locales/ca/views/chat.json +++ b/web/public/locales/ca/views/chat.json @@ -42,5 +42,28 @@ "show_camera_status": "Quin és l'estat actual de les meves càmeres?", "recap": "Què va passar mentre jo era fora?", "watch_camera": "Vigila la porta d'entrada i fes-me saber si algú apareix" + }, + "new_chat": "Xat nou", + "settings": { + "title": "Configuració del xat", + "show_stats": { + "title": "Mostra les estadístiques", + "desc": "Mostra la velocitat de generació i la mida del context per a les respostes del xat.", + "while_generating": "En generar", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Desplaçament automàtic", + "desc": "Segueix els missatges nous a mesura que arriben." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Raonant…", + "show": "Mostra el raonament", + "hide": "Amaga el raonament" } } diff --git a/web/public/locales/ca/views/faceLibrary.json b/web/public/locales/ca/views/faceLibrary.json index ea19924ac2..5f0546ecc8 100644 --- a/web/public/locales/ca/views/faceLibrary.json +++ b/web/public/locales/ca/views/faceLibrary.json @@ -14,7 +14,11 @@ "empty": "No hi ha intents recents de reconeixement de rostres", "title": "Reconeixements recents", "aria": "Selecciona els reconeixements recents", - "titleShort": "Recent" + "titleShort": "Recent", + "emptyNoLibrary": { + "title": "Puja una cara", + "description": "Heu d'afegir com a mínim una cara a la biblioteca perquè el reconeixement de la cara funcioni." + } }, "description": { "addFace": "Afegiu una col·lecció nova a la biblioteca de cares pujant la vostra primera imatge.", diff --git a/web/public/locales/ca/views/settings.json b/web/public/locales/ca/views/settings.json index b540b05861..ebd2278fd0 100644 --- a/web/public/locales/ca/views/settings.json +++ b/web/public/locales/ca/views/settings.json @@ -15,7 +15,8 @@ "globalConfig": "Configuració global - Frigate", "cameraConfig": "Configuració de la càmera - Frigate", "maintenance": "Manteniment - Frigate", - "profiles": "Perfils - Frigate" + "profiles": "Perfils - Frigate", + "detectorsAndModel": "Detectors i model - Frigate" }, "menu": { "ui": "Interfície d'usuari", @@ -90,7 +91,8 @@ "regionGrid": "Quadrícula de la regió", "uiSettings": "Paràmetres de la IU", "profiles": "Perfils", - "systemGo2rtcStreams": "go2rtc streams" + "systemGo2rtcStreams": "go2rtc streams", + "systemDetectorsAndModel": "Detectors i model" }, "dialog": { "unsavedChanges": { @@ -526,7 +528,7 @@ }, "title": "Afinador de detecció de moviment", "toast": { - "success": "Els ajustos de la detecció de moviment s'han desat." + "success": "S'han desat els paràmetres del moviment." }, "unsavedChanges": "Canvis no desats en l'ajust de moviment {{camera}}" }, @@ -724,7 +726,7 @@ "trainDate": "Data d'entrenament", "title": "Informació del model", "supportedDetectors": "Detectors compatibles", - "availableModels": "Models disponibles", + "availableModels": "Models Frigate+ disponibles", "cameras": "Càmeres", "plusModelType": { "userModel": "Afinat", @@ -733,7 +735,15 @@ "loadingAvailableModels": "Carregant models disponibles…", "loading": "Carregant informació del model…", "error": "No s'ha pogut carregar la informació del model", - "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector." + "modelSelect": "Els models disponibles a Frigate+ es poden seleccionar aquí. Tingues en compte que només es poden triar els models compatibles amb la configuració actual del detector.", + "noModelLoaded": "Actualment no s'ha carregat cap model Frigate+.", + "selectModel": "Selecciona un model", + "noModelsAvailable": "No hi ha models disponibles", + "filter": { + "ariaLabel": "Filtra els models per tipus", + "baseModels": "Models de base", + "fineTunedModels": "Models ajustats" + } }, "apiKey": { "plusLink": "Llegeix més sobre Frigate+", @@ -755,7 +765,8 @@ "currentModel": "Model actual", "otherModels": "Altres models", "configuration": "Configuració" - } + }, + "changeInDetectorsAndModel": "Canviar model" }, "enrichments": { "semanticSearch": { @@ -1295,7 +1306,7 @@ "title": "Habilita / Inhabilita les càmeres", "desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.
Nota: això no desactiva les retransmissions de go2rtc.", "enableLabel": "Càmeres habilitades", - "enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.
Nota: això no desactiva les retransmissions de go2rtc.", + "enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.
Nota: això no inhabilita els restreams go2rtc.

Drag el handle per reordenar les càmeres tal com apareixen a la interfície d'usuari. L'ordre de les càmeres habilitades es reflectirà en tota la interfície d'usuari, incloent el tauler en viu i els desplegables de selecció de càmeres.", "disableLabel": "Càmeres inhabilitades", "disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.", "enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis.", @@ -1304,7 +1315,10 @@ "title": "Edita el nom de la pantalla", "description": "Estableix el nom amigable que es mostra per a aquesta càmera a tota la interfície d'usuari de la Fragata. Deixeu-ho en blanc per utilitzar l'ID de la càmera.", "rename": "Canvia el nom" - } + }, + "reorderHandle": "Arrossega per reordenar", + "saving": "S'està desant…", + "saved": "Desat" }, "cameraConfig": { "add": "Afegeix una càmera", @@ -1362,7 +1376,8 @@ "dedicatedLpr": "LPR dedicat", "saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia la fragata per aplicar els canvis.", "normal": "Normal" - } + }, + "description": "Afegiu, editeu i suprimiu les càmeres, controleu quines càmeres estan habilitades, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera." }, "cameraReview": { "object_descriptions": { @@ -1661,7 +1676,9 @@ "options": { "embeddings": "Incrustació", "vision": "Visió", - "tools": "Eines" + "tools": "Eines", + "descriptions": "Descripcions", + "chat": "Xat" } }, "semanticSearchModel": { @@ -1718,7 +1735,10 @@ "saveAllPartial_many": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.", "saveAllPartial_other": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.", "saveAllFailure": "Ha fallat en desar totes les seccions.", - "applied": "La configuració s'ha aplicat correctament" + "applied": "La configuració s'ha aplicat correctament", + "saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia la fragata per aplicar els canvis.", + "saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis.", + "saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis." }, "unsavedChanges": "Teniu canvis sense desar", "confirmReset": "Confirma el restabliment", @@ -1743,7 +1763,15 @@ "othersField_many": "{{count}} altres", "othersField_other": "{{count}} altres", "profilePrefix": "Perfil {{profile}}: {{fields}}" - } + }, + "overriddenGlobalHeading_one": "Aquesta càmera substitueix el camp {{count}} de la configuració global:", + "overriddenGlobalHeading_many": "Aquesta càmera anul·la {{count}} camps de la configuració global:", + "overriddenGlobalHeading_other": "Aquesta càmera anul·la {{count}} camps de la configuració global:", + "overriddenGlobalNoDeltas": "Aquesta càmera anul·la la configuració global, però no hi ha valors de camp diferents.", + "overriddenBaseConfigHeading_one": "El perfil {{profile}} substitueix el camp {{count}} de la configuració base:", + "overriddenBaseConfigHeading_many": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:", + "overriddenBaseConfigHeading_other": "El perfil {{profile}} substitueix {{count}} camps de la configuració base:", + "overriddenBaseConfigNoDeltas": "El perfil {{profile}} substitueix aquesta secció, però no hi ha valors de camp diferents de la configuració base." }, "profiles": { "title": "Perfils", @@ -1827,8 +1855,17 @@ "audioMp3": "Transcodifica a MP3", "audioExclude": "Exclou", "hardwareNone": "Sense acceleració de hardware", - "hardwareAuto": "Acceleració de hardware automàtica" - } + "hardwareAuto": "Automàtic (recomanat)", + "addVideoCodec": "Afegeix un còdec de vídeo", + "addAudioCodec": "Afegeix un còdec d'àudio", + "removeCodec": "Elimina el còdec", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox" + }, + "streamNumber": "Flux {{index}}" }, "timestampPosition": { "tl": "A dalt a l'esquerra", @@ -1838,7 +1875,14 @@ }, "onvif": { "profileAuto": "Automàtic", - "profileLoading": "S'estan carregant perfils..." + "profileLoading": "S'estan carregant perfils...", + "autotracking": { + "zooming": { + "disabled": "Desactivat", + "absolute": "Absolut", + "relative": "Relatiu" + } + } }, "configMessages": { "review": { @@ -1886,5 +1930,104 @@ "semanticSearch": { "jinav2SmallModelSize": "La mida 'petita' amb el model Jina V2 té un alt cost de RAM i d'inferència. Es recomana el model 'gran' amb una GPU discreta." } + }, + "modelSize": { + "large": "Gran", + "small": "Petit" + }, + "birdseye": { + "trackingMode": { + "objects": "Objectes", + "motion": "Moviment", + "continuous": "Continu" + }, + "cameraOrder": { + "label": "Ordre de la càmera", + "description": "Arrossega les càmeres per establir el seu ordre en la disposició Birdseye.", + "reorderHandle": "Arrossega per reordenar", + "saving": "S'està desant…", + "saved": "Desat" + } + }, + "snapshot": { + "retainMode": { + "all": "Tots", + "motion": "Moviment", + "active_objects": "Objectes Actius" + } + }, + "ui": { + "timeFormat": { + "browser": "Visor", + "12hour": "12 hores", + "24hour": "24 hores" + }, + "TimeOrDateStyle": { + "full": "Complet", + "long": "Llarg", + "medium": "Mitjà", + "short": "Curt" + }, + "unitSystem": { + "metric": "Métric", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Gravacions", + "previews": "Previsualitzacions" + } + }, + "logger": { + "logLevel": { + "debug": "Depurar", + "info": "Informació", + "warning": "Avís", + "error": "Error", + "critical": "Crític" + } + }, + "retainMode": { + "all": "Tots", + "motion": "Moviment", + "active_objects": "Objectes actius" + }, + "previewQuality": { + "very_high": "Molt alta", + "high": "Alta", + "medium": "Mitja", + "low": "Baix", + "very_low": "Molt baix" + }, + "detectorsAndModel": { + "restartRequired": "Reinici requerit (canvi en detector o model)", + "title": "Detectors i model", + "description": "Configuri el detector final que corre la detecció d'objectes i el model que usa. Els canvis es gravaràn junts i així el detector i el model estan sincronitzats.", + "cardTitles": { + "detector": "Detector Hardware", + "model": "Model de detecció" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Model personalitzat" + }, + "mismatch": { + "warning": "El model actual de Frigate+ \"{{model}}\" requereix el detector {{required}}. Selecciona un model compatible a baix o canvía e model personalitzat abans de gravar." + }, + "plusModel": { + "requiresDetector": "Requereix: {{detector}}", + "noModelSelected": "Selecciona un model Frigate+" + }, + "toast": { + "saveSuccess": "Configuració de detectors i model guardats. Reinicia Frigate per aplicar els canvis.", + "saveError": "Fallo en gravar la configuració de detector i model" + }, + "unsavedChanges": "Canvis de detector i model no gravats" + }, + "menuDot": { + "overrideGlobal": "Aquesta secció substitueix la configuració global", + "overrideProfile": "Aquesta secció està substituïda pel perfil {{profile}}", + "unsaved": "Aquesta secció té canvis sense desar" } } From cfb14206605587ffcbd714a66bf80504b8cb110e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:48 +0200 Subject: [PATCH 17/94] Translated using Weblate (Portuguese) Currently translated at 100.0% (2 of 2 strings) Co-authored-by: Hosted Weblate Co-authored-by: ssantos Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/pt/ Translation: Frigate NVR/components-input --- web/public/locales/pt/components/input.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/public/locales/pt/components/input.json b/web/public/locales/pt/components/input.json index 1324ed188a..9861b92f5a 100644 --- a/web/public/locales/pt/components/input.json +++ b/web/public/locales/pt/components/input.json @@ -1,9 +1,9 @@ { "button": { "downloadVideo": { - "label": "Transferir Vídeo", + "label": "Descarregar Vídeo", "toast": { - "success": "O vídeo do seu item de análise começou a ser transferido." + "success": "O vídeo do seu item de análise começou a ser descarregado." } } } From f4cbbe806d7af70c0547a0dc9db725c312c131d0 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:49 +0200 Subject: [PATCH 18/94] Translated using Weblate (Polish) Currently translated at 91.9% (218 of 237 strings) Translated using Weblate (Polish) Currently translated at 63.5% (731 of 1150 strings) Co-authored-by: Hosted Weblate Co-authored-by: J P Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/pl/ Translation: Frigate NVR/common Translation: Frigate NVR/views-settings --- web/public/locales/pl/common.json | 3 +- web/public/locales/pl/views/settings.json | 102 +++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/web/public/locales/pl/common.json b/web/public/locales/pl/common.json index e6fea5b424..9007e9cd50 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -260,7 +260,8 @@ "help": "Pomoc", "settings": "Ustawienia", "export": "Eksportuj", - "classification": "Klasyfikacja" + "classification": "Klasyfikacja", + "profiles": "Profile" }, "role": { "viewer": "Przeglądający", diff --git a/web/public/locales/pl/views/settings.json b/web/public/locales/pl/views/settings.json index 38cb2cc4ac..6167a9d0e9 100644 --- a/web/public/locales/pl/views/settings.json +++ b/web/public/locales/pl/views/settings.json @@ -13,7 +13,62 @@ "triggers": "Wyzwalacze", "roles": "Role", "cameraManagement": "Zarządzanie", - "cameraReview": "Przegląd" + "cameraReview": "Przegląd", + "integrations": "Integracje", + "uiSettings": "Ustawienia interfejsu użytkownika", + "profiles": "Profile", + "globalDetect": "Detekcja obiektów", + "globalRecording": "Nagrywanie", + "globalSnapshots": "Snapshoty", + "globalFfmpeg": "FFmpeg", + "globalMotion": "Detekcja ruchu", + "globalObjects": "Obiekty", + "globalReview": "Recenzja", + "globalAudioEvents": "Detekcja dźwięku", + "globalLivePlayback": "Podgląd na żywo", + "globalTimestampStyle": "Styl znacznika czasu", + "systemDatabase": "Baza danych", + "systemTls": "TLS", + "systemAuthentication": "Autentykacja", + "systemNetworking": "Sieć", + "systemProxy": "Proxy", + "systemUi": "UI", + "systemLogging": "Logowanie", + "systemEnvironmentVariables": "Zmienne środowiskowe", + "systemTelemetry": "Telemetria", + "systemBirdseye": "Podgląd obrazu", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detektory i model", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "strumienie go2rtc", + "integrationSemanticSearch": "Wyszukiwanie semantyczne", + "integrationGenerativeAi": "Generatywna sztuczna inteligencja", + "integrationFaceRecognition": "Rozpoznawanie twarzy", + "integrationLpr": "Rozpoznawanie tablic rejestracyjnych", + "integrationObjectClassification": "Klasyfikacja obiektów", + "integrationAudioTranscription": "Transkrypcja dźwięku", + "cameraDetect": "Detekcja obiektów", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Nagrywanie", + "cameraBirdseye": "Podgląd obrazu", + "cameraFaceRecognition": "Rozpoznawanie twarzy", + "cameraLpr": "Rozpoznawanie tablic rejestracyjnych", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraAudioTranscription": "Transkrypcja dźwięku", + "cameraNotifications": "Powiadomienia", + "cameraLivePlayback": "Podgląd na żywo", + "cameraSnapshots": "Snapshoty", + "cameraMotion": "Detekcja ruchu", + "cameraObjects": "Obiekty", + "cameraConfigReview": "Recenzja", + "cameraAudioEvents": "Detekcja dźwięku", + "cameraUi": "Interfejs użytkownika kamery", + "cameraTimestampStyle": "Styl znacznika czasu", + "cameraMqtt": "MQTT kamery", + "maintenance": "Utrzymanie", + "mediaSync": "Synchronizacja mediów", + "regionGrid": "Siatka regionalna" }, "dialog": { "unsavedChanges": { @@ -100,7 +155,8 @@ "globalConfig": "Konfiguracja globalna - Frigate", "cameraConfig": "Konfiguracja kamery - Frigate", "maintenance": "Konserwacja – Frigate", - "profiles": "Profile - Frigate" + "profiles": "Profile - Frigate", + "detectorsAndModel": "Detektory i model" }, "classification": { "title": "Ustawienia Klasyfikacji", @@ -1231,5 +1287,47 @@ "title": "Opisy generatywnej sztucznej inteligencji", "desc": "Tymczasowo włącz/wyłącz generatywne opisy AI dla tej kamery. Po wyłączeniu opisy generowane przez AI nie będą wymagane dla elementów przeglądu w tej kamerze." } + }, + "button": { + "overriddenGlobal": "Nadpisane (globalnie)", + "overriddenGlobalTooltip": "Ta kamera nadpisuje globalną konfigurację w tej sekcji", + "overriddenGlobalHeading_one": "Ta kamera nadpisuje pole {{count}} z globalnej konfiguracji:", + "overriddenGlobalHeading_few": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:", + "overriddenGlobalHeading_many": "Ta kamera nadpisuje pola {{count}} z globalnej konfiguracji:", + "overriddenGlobalNoDeltas": "Ta kamera nadpisuje ustawienia globalne, ale żadne wartości pól się nie różnią.", + "overriddenBaseConfig": "Nadpisane (bazowa konfiguracja)", + "overriddenBaseConfigTooltip": "Profil {{profile}} zastępuje ustawienia konfiguracyjne w tej sekcji", + "overriddenBaseConfigHeading_one": "Profil {{profile}} zastępuje pole {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigHeading_few": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigHeading_many": "Profile {{profile}} zastępują pola {{count}} z konfiguracji podstawowej:", + "overriddenBaseConfigNoDeltas": "Profil {{profile}} zastępuje tę sekcję, ale żadne wartości pól nie różnią się od konfiguracji podstawowej.", + "overriddenInCameras": { + "label_one": "Zastąpiono w kamerze {{count}}", + "label_few": "Zastąpiono w kamerach {{count}}", + "label_many": "Zastąpiono w kamerach {{count}}", + "tooltip_one": "Kamera {{count}} zastępuje wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.", + "tooltip_few": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły.", + "tooltip_many": "Kamery {{count}} zastępują wartości w tej sekcji. Kliknij, aby wyświetlić szczegóły." + } + }, + "saveAllPreview": { + "scope": { + "label": "Zakres", + "global": "Globalne", + "camera": "Kamera: {{cameraName}}" + }, + "profile": { + "label": "Profil" + }, + "field": { + "label": "Pole" + }, + "value": { + "label": "Nowa wartość", + "reset": "Reset" + }, + "title": "Zmiany do zapisania", + "triggerLabel": "Przejrzyj oczekujące zmiany", + "empty": "Brak oczekujących zmian." } } From 3df7c22f4d76be63e0eab172d762d8b83f8d106f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:51 +0200 Subject: [PATCH 19/94] Translated using Weblate (Italian) Currently translated at 27.7% (220 of 794 strings) Translated using Weblate (Italian) Currently translated at 24.9% (118 of 473 strings) Translated using Weblate (Italian) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Italian) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Italian) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Italian) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Italian) Currently translated at 100.0% (100 of 100 strings) Translated using Weblate (Italian) Currently translated at 100.0% (175 of 175 strings) Translated using Weblate (Italian) Currently translated at 100.0% (64 of 64 strings) Translated using Weblate (Italian) Currently translated at 24.8% (197 of 794 strings) Translated using Weblate (Italian) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Italian) Currently translated at 20.0% (95 of 473 strings) Translated using Weblate (Italian) Currently translated at 77.3% (882 of 1141 strings) Co-authored-by: Gringo Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/it/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-events Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/it/common.json | 3 +- web/public/locales/it/config/cameras.json | 64 ++- web/public/locales/it/config/global.json | 65 ++- web/public/locales/it/objects.json | 6 +- web/public/locales/it/views/chat.json | 18 + web/public/locales/it/views/events.json | 2 +- web/public/locales/it/views/faceLibrary.json | 6 +- web/public/locales/it/views/live.json | 2 +- web/public/locales/it/views/settings.json | 481 ++++++++++++++++++- web/public/locales/it/views/system.json | 2 +- 10 files changed, 618 insertions(+), 31 deletions(-) diff --git a/web/public/locales/it/common.json b/web/public/locales/it/common.json index f571f11a94..1a718250be 100644 --- a/web/public/locales/it/common.json +++ b/web/public/locales/it/common.json @@ -221,7 +221,8 @@ "gl": "Galego (Galiziano)", "id": "Bahasa Indonesia (Indonesiano)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Croato)" + "hr": "Hrvatski (Croato)", + "bs": "Bosanski (Bosniaco)" }, "darkMode": { "label": "Modalità scura", diff --git a/web/public/locales/it/config/cameras.json b/web/public/locales/it/config/cameras.json index ebf816b539..01dddb58a1 100644 --- a/web/public/locales/it/config/cameras.json +++ b/web/public/locales/it/config/cameras.json @@ -33,7 +33,11 @@ }, "filters": { "label": "Filtri audio", - "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi." + "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.", + "threshold": { + "label": "Affidabilità audio minima", + "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato." + } }, "enabled_in_config": { "label": "Stato audio originale", @@ -129,7 +133,44 @@ } }, "detect": { - "label": "Rilevamento oggetti" + "label": "Rilevamento oggetti", + "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.", + "enabled": { + "label": "Abilita il rilevamento degli oggetti", + "description": "Abilita o disabilita il rilevamento degli oggetti per questa telecamera." + }, + "height": { + "label": "Rileva altezza", + "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "width": { + "label": "Rileva larghezza", + "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "fps": { + "label": "Rileva FPS", + "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)." + }, + "min_initialized": { + "label": "Frame di inizializzazione minimi", + "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2." + }, + "max_disappeared": { + "label": "Numero di fotogrammi scomparsi", + "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso." + }, + "stationary": { + "label": "Configurazione degli oggetti stazionari", + "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.", + "interval": { + "label": "Intervallo stazionario", + "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario." + }, + "threshold": { + "label": "Soglia stazionaria", + "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario." + } + } }, "face_recognition": { "label": "Riconoscimento facciale" @@ -189,7 +230,20 @@ } }, "birdseye": { - "label": "Birdseye" + "label": "Birdseye", + "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.", + "enabled": { + "label": "Abilita Birdseye", + "description": "Abilita o disabilita la funzione di visualizzazione Birdseye." + }, + "mode": { + "label": "Modalità di tracciamento", + "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'." + }, + "order": { + "label": "Posizione", + "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye." + } }, "semantic_search": { "label": "Ricerca semantica", @@ -216,5 +270,9 @@ "label": "Abilitata" }, "label": "Zone" + }, + "type": { + "description": "Tipo di telecamera", + "label": "Tipo di telecamera" } } diff --git a/web/public/locales/it/config/global.json b/web/public/locales/it/config/global.json index 5bb38cf431..d7e594ace1 100644 --- a/web/public/locales/it/config/global.json +++ b/web/public/locales/it/config/global.json @@ -30,7 +30,11 @@ }, "filters": { "label": "Filtri audio", - "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi." + "description": "Impostazioni di filtro per ciascun tipo di audio, come le soglie di confidenza utilizzate per ridurre i falsi positivi.", + "threshold": { + "label": "Affidabilità audio minima", + "description": "Soglia minima di fiducia affinché l'evento audio venga conteggiato." + } }, "enabled_in_config": { "label": "Stato audio originale", @@ -254,7 +258,44 @@ } }, "detect": { - "label": "Rilevamento oggetti" + "label": "Rilevamento oggetti", + "description": "Impostazioni per il ruolo di rilevamento/rilevamento utilizzato per eseguire il rilevamento degli oggetti e inizializzare i localizzatori.", + "enabled": { + "label": "Abilita il rilevamento degli oggetti", + "description": "Abilita o disabilita il rilevamento degli oggetti per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." + }, + "height": { + "label": "Rileva altezza", + "description": "Altezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "width": { + "label": "Rileva larghezza", + "description": "Larghezza (in pixel) dei fotogrammi utilizzati per il flusso di rilevamento; lascia vuoto per utilizzare la risoluzione nativa del flusso." + }, + "fps": { + "label": "Rileva FPS", + "description": "Numero di fotogrammi al secondo desiderati per eseguire il rilevamento; valori inferiori riducono l'utilizzo della CPU (il valore consigliato è 5, impostarne uno superiore - al massimo 10 - solo se si devono tracciare oggetti in movimento estremamente rapidi)." + }, + "min_initialized": { + "label": "Frame di inizializzazione minimi", + "description": "Numero di rilevamenti consecutivi necessari prima di creare un oggetto tracciato. Aumenta questo valore per ridurre le inizializzazioni errate. Il valore predefinito è FPS diviso per 2." + }, + "max_disappeared": { + "label": "Numero di fotogrammi scomparsi", + "description": "Numero di fotogrammi senza rilevamento prima che un oggetto tracciato venga considerato scomparso." + }, + "stationary": { + "label": "Configurazione degli oggetti stazionari", + "description": "Impostazioni per rilevare e gestire gli oggetti che rimangono fermi per un certo periodo di tempo.", + "interval": { + "label": "Intervallo stazionario", + "description": "Con quale frequenza (in fotogrammi) eseguire un controllo di rilevamento per confermare che l'oggetto sia stazionario." + }, + "threshold": { + "label": "Soglia stazionaria", + "description": "Numero di fotogrammi senza cambio di posizione necessari per contrassegnare un oggetto come stazionario." + } + } }, "face_recognition": { "label": "Riconoscimento facciale", @@ -351,7 +392,7 @@ }, "notifications": { "label": "Notifiche", - "description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere modificate per ogni singola telecamera.", + "description": "Impostazioni per abilitare e controllare le notifiche per tutte le telecamere; possono essere sovrascritte per ogni singola telecamera.", "enabled": { "label": "Abilita le notifiche", "description": "Abilita o disabilita le notifiche per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." @@ -400,7 +441,20 @@ "label": "Telemetria" }, "birdseye": { - "label": "Birdseye" + "label": "Birdseye", + "description": "Impostazioni per la vista composita Birdseye che unisce più flussi video di telecamere in un unico formato.", + "enabled": { + "label": "Abilita Birdseye", + "description": "Abilita o disabilita la funzione di visualizzazione Birdseye." + }, + "mode": { + "label": "Modalità di tracciamento", + "description": "Modalità per includere le telecamere in Birdseye: 'oggetti', 'movimento' o 'continuo'." + }, + "order": { + "label": "Posizione", + "description": "Posizione numerica che controlla l'ordine delle telecamere nella disposizione Birdseye." + } }, "model": { "label": "Modello di rilevamento" @@ -445,5 +499,8 @@ "label": "Mostra nell'interfaccia utente", "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente." } + }, + "active_profile": { + "label": "Profilo attivo" } } diff --git a/web/public/locales/it/objects.json b/web/public/locales/it/objects.json index 069acd07be..230931d635 100644 --- a/web/public/locales/it/objects.json +++ b/web/public/locales/it/objects.json @@ -121,5 +121,9 @@ "royal_mail": "Royal Mail", "school_bus": "Autobus scolastico", "skunk": "Puzzola", - "kangaroo": "Canguro" + "kangaroo": "Canguro", + "baby": "Bambino", + "baby_stroller": "Passeggino per bambini", + "rickshaw": "Risciò", + "rodent": "Roditore" } diff --git a/web/public/locales/it/views/chat.json b/web/public/locales/it/views/chat.json index 67b93ffdd5..a56c5a4ee1 100644 --- a/web/public/locales/it/views/chat.json +++ b/web/public/locales/it/views/chat.json @@ -42,5 +42,23 @@ "show_camera_status": "Qual è lo stato attuale delle mie telecamere?", "recap": "Cosa è successo mentre ero via?", "watch_camera": "Controlla la porta d'ingresso e fammi sapere se arriva qualcuno" + }, + "new_chat": "Nuova chat", + "settings": { + "title": "Impostazioni chat", + "show_stats": { + "title": "Mostra statistiche", + "desc": "Mostra la frequenza di generazione e la dimensione del contesto per le risposte in chat.", + "while_generating": "Durante la generazione", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Scorrimento automatico", + "desc": "Segui i nuovi messaggi non appena arrivano." + } + }, + "stats": { + "context": "{{tokens}} token", + "tokens_per_second": "{{rate}} t/s" } } diff --git a/web/public/locales/it/views/events.json b/web/public/locales/it/views/events.json index 45289b6452..4a31d9526f 100644 --- a/web/public/locales/it/views/events.json +++ b/web/public/locales/it/views/events.json @@ -2,7 +2,7 @@ "alerts": "Avvisi", "detections": "Rilevamenti", "motion": { - "label": "Movimenti", + "label": "Movimento", "only": "Solo movimenti" }, "empty": { diff --git a/web/public/locales/it/views/faceLibrary.json b/web/public/locales/it/views/faceLibrary.json index 12d640aa8f..842881fc69 100644 --- a/web/public/locales/it/views/faceLibrary.json +++ b/web/public/locales/it/views/faceLibrary.json @@ -20,7 +20,11 @@ "title": "Riconoscimenti recenti", "aria": "Seleziona i riconoscimenti recenti", "empty": "Non ci sono recenti tentativi di riconoscimento facciale", - "titleShort": "Recente" + "titleShort": "Recente", + "emptyNoLibrary": { + "title": "Carica un volto", + "description": "Affinché il riconoscimento facciale funzioni è necessario aggiungere almeno un volto alla libreria." + } }, "button": { "addFace": "Aggiungi volto", diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json index 466110ad10..45696b1256 100644 --- a/web/public/locales/it/views/live.json +++ b/web/public/locales/it/views/live.json @@ -158,7 +158,7 @@ }, "effectiveRetainMode": { "modes": { - "all": "Tutto", + "all": "Tutti", "motion": "Movimento", "active_objects": "Oggetti attivi" }, diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json index 2b4aea94e8..042c4694e0 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -52,7 +52,15 @@ "modelType": "Tipo di modello", "modelSelect": "Qui puoi selezionare i modelli disponibili su Frigate+. Nota: puoi selezionare solo i modelli compatibili con la configurazione attuale del tuo rilevatore.", "title": "Informazioni sul modello", - "loading": "Caricamento informazioni sul modello…" + "loading": "Caricamento informazioni sul modello…", + "noModelsAvailable": "Nessun modello disponibile", + "noModelLoaded": "Al momento non è caricato alcun modello di Frigate+.", + "selectModel": "Seleziona un modello", + "filter": { + "ariaLabel": "Filtra i modelli per tipo", + "baseModels": "Modelli base", + "fineTunedModels": "Modelli ottimizzati" + } }, "toast": { "error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}", @@ -203,6 +211,10 @@ "zone": "zona", "motion_mask": "maschera di movimento", "object_mask": "maschera di oggetto" + }, + "revertOverride": { + "title": "Ripristina la configurazione di base", + "desc": "Questo rimuoverà la sovrascrittura del profilo per {{type}} {{name}} e ripristinerà la configurazione di base." } }, "inertia": { @@ -219,6 +231,17 @@ "error": { "mustBeGreaterOrEqualTo": "La soglia di velocità deve essere maggiore o uguale a 0,1." } + }, + "id": { + "error": { + "mustNotBeEmpty": "L'ID non deve essere vuoto.", + "alreadyExists": "Esiste già una maschera con questo ID per questa telecamera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Il nome non deve essere vuoto." + } } }, "filter": { @@ -329,7 +352,11 @@ "title": "Abilitata", "description": "Indica se questa maschera è abilitata nel file di configurazione. Se disabilitata, non può essere abilitata tramite MQTT. Le maschere disabilitate vengono ignorate in fase di esecuzione." } - } + }, + "disabledInConfig": "L'elemento è disabilitato nel file di configurazione", + "addDisabledProfile": "Aggiungi prima alla configurazione di base, poi sovrascrivi nel profilo", + "profileBase": "(base)", + "profileOverride": "(sovrascrivi)" }, "cameraSetting": { "camera": "Telecamera", @@ -477,7 +504,10 @@ "cameraOnvif": "ONVIF", "cameraTimestampStyle": "Stile orario", "cameraUi": "Interfaccia utente telecamera", - "mediaSync": "Sincronizzazione multimediale" + "mediaSync": "Sincronizzazione multimediale", + "cameraMqtt": "MQTT telecamera", + "maintenance": "Manutenzione", + "regionGrid": "Griglia di regioni" }, "users": { "dialog": { @@ -1359,7 +1389,8 @@ }, "hikvision": { "substreamWarning": "Il sottoflusso 1 è bloccato a bassa risoluzione. Molte telecamere Hikvision supportano sottoflussi aggiuntivi che devono essere abilitati nelle impostazioni della telecamera. Si consiglia di controllare e utilizzare tali flussi, se disponibili." - } + }, + "resolutionUnknown": "Non è stato possibile rilevare la risoluzione di questo flusso. È necessario impostare manualmente la risoluzione di rilevamento nelle Impostazioni o nella configurazione." } } }, @@ -1414,7 +1445,33 @@ "addGo2rtcStream": "Aggiungi flusso go2rtc" }, "profiles": { - "enabled": "Abilitato" + "enabled": "Abilitato", + "title": "Sovrascritture della telecamera del profilo", + "selectLabel": "Seleziona il profilo", + "description": "Configura quali telecamere vengono abilitate o disabilitate all'attivazione di un profilo. Le telecamere impostate su \"Eredita\" mantengono il loro stato di abilitazione predefinito.", + "inherit": "Eredita", + "disabled": "Disabilitato" + }, + "description": "Aggiungi, modifica ed elimina le telecamere, controlla quali telecamere sono abilitate e configura le impostazioni personalizzate per profilo e tipo di telecamera. Per configurare flussi video, rilevamento, movimento e altre impostazioni specifiche per ciascuna telecamera, seleziona la sezione corrispondente in Configurazione telecamera.", + "deleteCamera": "Elimina telecamera", + "deleteCameraDialog": { + "title": "Elimina telecamera", + "description": "L'eliminazione di una telecamera rimuoverà in modo permanente tutte le registrazioni, gli oggetti tracciati e la configurazione relativi a tale telecamera. Potrebbe essere comunque necessario rimuovere manualmente gli eventuali flussi go2rtc associati a questa telecamera.", + "selectPlaceholder": "Scegli telecamera...", + "confirmTitle": "Sei sicuro?", + "confirmWarning": "L'eliminazione di {{cameraName}} è irreversibile.", + "deleteExports": "Elimina anche le esportazioni per questa telecamera", + "confirmButton": "Elimina definitivamente", + "success": "Telecamera {{cameraName}} eliminata con successo", + "error": "Impossibile eliminare la telecamera {{cameraName}}" + }, + "cameraType": { + "title": "Tipo di telecamera", + "label": "Tipo di telecamera", + "description": "Imposta il tipo per ogni telecamera. Le telecamere LPR dedicate sono telecamere monouso con un potente zoom ottico per acquisire le targhe dei veicoli distanti. La maggior parte delle telecamere dovrebbe utilizzare il tipo di telecamera normale, a meno che non siano specificamente progettate per il riconoscimento delle targhe e abbiano una visuale molto ravvicinata sulle targhe.", + "normal": "Normale", + "dedicatedLpr": "LPR dedicata", + "saveSuccess": "Tipo di telecamera aggiornato per {{cameraName}}. Riavviare Frigate per applicare le modifiche." } }, "button": { @@ -1436,7 +1493,15 @@ "othersField_many": "{{count}} altri", "othersField_other": "{{count}} altri", "profilePrefix": "Profilo {{profile}}: {{fields}}" - } + }, + "overriddenGlobalHeading_one": "Questa telecamera sovrascrive il campo {{count}} dalla configurazione globale:", + "overriddenGlobalHeading_many": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:", + "overriddenGlobalHeading_other": "Questa telecamera sovrascrive i campi {{count}} della configurazione globale:", + "overriddenGlobalNoDeltas": "Questa telecamera sovrascrive la configurazione globale, ma nessun valore dei campi risulta diverso.", + "overriddenBaseConfigHeading_one": "Il profilo {{profile}} sovrascrive il campo {{count}} della configurazione di base:", + "overriddenBaseConfigHeading_many": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:", + "overriddenBaseConfigHeading_other": "Il profilo {{profile}} sovrascrive i campi {{count}} della configurazione di base:", + "overriddenBaseConfigNoDeltas": "Il profilo {{profile}} sovrascrive questa sezione, ma nessun valore di campo differisce dalla configurazione di base." }, "go2rtcStreams": { "title": "Flussi go2rtc", @@ -1447,8 +1512,38 @@ "videoCopy": "Copia", "hardware": "Accelerazione hardware", "hardwareNone": "Nessuna accelerazione hardware", - "hardwareAuto": "Accelerazione hardware automatica" - } + "hardwareAuto": "Accelerazione hardware automatica", + "useFfmpegModule": "Utilizza la modalità di compatibilità (ffmpeg)", + "videoH264": "Transcodifica in H.264", + "videoH265": "Transcodifica in H.265", + "videoExclude": "Escludi", + "audioAac": "Transcodifica in AAC", + "audioOpus": "Transcodifica in Opus", + "audioPcmu": "Transcodifica in PCM μ-law", + "audioPcma": "Transcodifica in PCM A-law", + "audioPcm": "Transcodifica in PCM", + "audioMp3": "Transcodifica in MP3", + "audioExclude": "Escludi" + }, + "description": "Gestisci le configurazioni del flusso go2rtc per la ritrasmissione delle immagini della telecamera. Ogni flusso ha un nome e uno o più URL sorgente.", + "addStream": "Aggiungi flusso", + "addStreamDesc": "Inserisci un nome per il nuovo flusso. Questo nome verrà utilizzato per identificare il flusso nella configurazione della telecamera.", + "addUrl": "Aggiungi URL", + "streamName": "Nome flusso", + "streamNamePlaceholder": "p. es., porta_ingresso", + "streamUrlPlaceholder": "p. es., rtsp://utente:password@192.168.1.100/flusso", + "deleteStream": "Elimina flusso", + "deleteStreamConfirm": "Sei sicuro di voler eliminare il flusso \"{{streamName}}\"? Le telecamere che fanno riferimento a questo flusso potrebbero smettere di funzionare.", + "noStreams": "Nessun flusso go2rtc configurato. Aggiungi un flusso per iniziare.", + "validation": { + "nameRequired": "Il nome del flusso è obbligatorio", + "nameDuplicate": "Esiste già un flusso con questo nome", + "nameInvalid": "Il nome del flusso può contenere solo lettere, numeri, trattini bassi e trattini", + "urlRequired": "È richiesto almeno un URL" + }, + "renameStream": "Rinomina flusso", + "renameStreamDesc": "Inserisci un nuovo nome per questo flusso. Rinominare un flusso potrebbe causare problemi alle telecamere o ad altri flussi che lo referenziano tramite il suo nome.", + "newStreamName": "Nuovo nome del flusso" }, "configForm": { "sections": { @@ -1470,7 +1565,14 @@ "face_recognition": "Riconoscimento facciale", "masksAndZones": "Maschere / Zone", "audio": "Audio", - "model": "Modello" + "model": "Modello", + "detect": "Rilevamento", + "motion": "Movimento", + "live": "Vista dal vivo", + "timestamp_style": "Orari", + "go2rtc": "go2rtc", + "detectors": "Rivelatori", + "genai": "GenAI" }, "tabs": { "system": "Sistema", @@ -1479,12 +1581,19 @@ }, "inputRoles": { "options": { - "audio": "Audio" - } + "audio": "Audio", + "detect": "Rileva", + "record": "Registra" + }, + "summary": "{{count}} ruoli selezionati", + "empty": "Nessun ruolo disponibile" }, "roleMap": { "roleLabel": "Ruolo", - "remove": "Rimuovi" + "remove": "Rimuovi", + "empty": "Nessuna mappatura dei ruoli", + "groupsLabel": "Gruppi", + "addMapping": "Aggiungi la mappatura dei ruoli" }, "notifications": { "title": "Impostazioni di notifica" @@ -1518,19 +1627,179 @@ "presetLabels": { "preset-rpi-64-h264": "Raspberry Pi (H.264)", "preset-rpi-64-h265": "Raspberry Pi (H.265)", - "preset-vaapi": "VAAPI (GPU Intel/AMD)" + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generico)", + "preset-http-mjpeg-generic": "HTTP JPEG (Generico)", + "preset-http-reolink": "HTTP - Telecamere Reolink", + "preset-rtmp-generic": "RTMP (Generico)", + "preset-rtsp-generic": "RTSP (Generico)", + "preset-rtsp-restream": "RTSP - Ritrasmissione da go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Ritrasmissione da go2rtc (bassa latenza)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Registrazione (Generica, senza audio)", + "preset-record-generic-audio-copy": "Registrazione (generica + copia audio)", + "preset-record-generic-audio-aac": "Registrazione (generica + audio in AAC)", + "preset-record-mjpeg": "Registrazione - Telecamere MJPEG", + "preset-record-jpeg": "Registrazione - Telecamere JPEG", + "preset-record-ubiquiti": "Registrazione - Telecamere Ubiquiti" } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}" + }, + "restartRequiredField": "Riavvio richiesto", + "restartRequiredFooter": "Configurazione modificata - Riavvio necessario", + "detect": { + "title": "Impostazioni di rilevamento" + }, + "detectors": { + "title": "Impostazioni del rilevatore", + "singleType": "È consentito un solo rilevatore di tipo {{type}}.", + "keyRequired": "Il nome del rilevatore è obbligatorio.", + "keyDuplicate": "Il nome del rilevatore esiste già.", + "noSchema": "Non sono disponibili schemi di rilevamento.", + "none": "Nessuna istanza del rilevatore configurata.", + "add": "Aggiungi rilevatore", + "addCustomKey": "Aggiungi chiave personalizzata" + }, + "record": { + "title": "Impostazioni di registrazione" + }, + "snapshots": { + "title": "Impostazioni istantanea" + }, + "motion": { + "title": "Impostazioni di movimento" + }, + "objects": { + "title": "Impostazioni oggetto" + }, + "audioLabels": { + "summary": "{{count}} etichette audio selezionate", + "empty": "Nessuna etichetta audio disponibile" + }, + "objectLabels": { + "summary": "{{count}} tipi di oggetto selezionati", + "empty": "Non sono disponibili etichette per gli oggetti" + }, + "reviewLabels": { + "summary": "{{count}} etichette selezionate", + "empty": "Nessuna etichetta disponibile" + }, + "filters": { + "objectFieldLabel": "{{field}} per {{label}}" + }, + "zoneNames": { + "summary": "{{count}} selezionati", + "empty": "Nessuna zona disponibile" + }, + "genaiRoles": { + "options": { + "embeddings": "Incorporamento", + "descriptions": "Descrizioni", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Seleziona il modello…", + "builtIn": "Modelli integrati", + "genaiProviders": "Fornitori di GenAI" + }, + "genaiModel": { + "placeholder": "Seleziona il modello…", + "search": "Ricerca modelli…", + "noModels": "Nessun modello disponibile" + }, + "review": { + "title": "Impostazioni di revisione" + }, + "audio": { + "title": "Impostazioni audio" + }, + "live": { + "title": "Impostazioni della visualizzazione dal vivo" + }, + "timestamp_style": { + "title": "Impostazioni orario" + }, + "searchPlaceholder": "Ricerca...", + "addCustomLabel": "Aggiungi etichetta personalizzata...", + "knownPlates": { + "namePlaceholder": "p. es., auto della moglie", + "platePlaceholder": "Numero di targa o espressione regolare" + }, + "timezone": { + "defaultOption": "Utilizza il fuso orario del browser" } }, "globalConfig": { - "title": "Configurazione globale" + "title": "Configurazione globale", + "description": "Configura le impostazioni globali che si applicano a tutte le telecamere, a meno che non vengano sovrascritte.", + "toast": { + "success": "Impostazioni globali salvate correttamente", + "error": "Impossibile salvare le impostazioni globali", + "validationError": "Validazione fallita" + } }, "cameraConfig": { - "title": "Configurazione telecamera" + "title": "Configurazione telecamera", + "description": "Configura le impostazioni per le singole telecamere. Le impostazioni personalizzate sovrascrivono le impostazioni predefinite globali.", + "overriddenBadge": "Sovrascritto", + "resetToGlobal": "Ripristina impostazioni globali", + "toast": { + "success": "Impostazioni della telecamera salvate correttamente", + "error": "Impossibile salvare le impostazioni della telecamera" + } }, "profiles": { "title": "Profili", - "columnCamera": "Telecamera" + "columnCamera": "Telecamera", + "activeProfile": "Profilo attivo", + "noActiveProfile": "Nessun profilo attivo", + "active": "Attivo", + "activated": "Profilo '{{profile}}' attivato", + "activateFailed": "Impossibile impostare il profilo", + "deactivated": "Profilo disattivato", + "noProfiles": "Nessun profilo definito.", + "noOverrides": "Nessuna sovrascrittura", + "cameraCount_one": "{{count}} telecamera", + "cameraCount_many": "{{count}} telecamere", + "cameraCount_other": "{{count}} telecamere", + "columnOverrides": "Sovrascritture del profilo", + "baseConfig": "Configurazione di base", + "addProfile": "Aggiungi profilo", + "newProfile": "Nuovo profilo", + "profileNamePlaceholder": "p. es., Inserita, Assente, Modalità notturna", + "friendlyNameLabel": "Nome profilo", + "profileIdLabel": "ID profilo", + "profileIdDescription": "Identificativo interno utilizzato nella configurazione e nelle automazioni", + "nameInvalid": "Sono consentite solo lettere minuscole, numeri e trattini bassi", + "nameDuplicate": "Esiste già un profilo con questo nome", + "error": { + "mustBeAtLeastTwoCharacters": "Deve contenere almeno 2 caratteri", + "mustNotContainPeriod": "Non deve contenere punti", + "alreadyExists": "Esiste già un profilo con questo ID" + }, + "renameProfile": "Rinomina profilo", + "renameSuccess": "Profilo rinominato in '{{profile}}'", + "deleteProfile": "Elimina profilo", + "deleteProfileConfirm": "Eliminare il profilo \"{{profile}}\" da tutte le telecamere? Questa operazione non può essere annullata.", + "deleteSuccess": "Profilo '{{profile}}' eliminato", + "createSuccess": "Profilo '{{profile}}' creato", + "removeOverride": "Rimuovi la sovrascrittura del profilo", + "deleteSection": "Elimina le sostituzioni della sezione", + "deleteSectionConfirm": "Rimuovere le sovrascritture {{section}} per il profilo {{profile}} su {{camera}}?", + "deleteSectionSuccess": "Rimosse le sovrascritture di {{section}} per {{profile}}", + "enableSwitch": "Abilita profili", + "enabledDescription": "I profili sono abilitati. Crea un nuovo profilo qui sotto, vai alla sezione di configurazione della telecamera per apportare le modifiche e salva affinché le modifiche abbiano effetto.", + "disabledDescription": "I profili consentono di definire insiemi denominati di impostazioni di configurazione della telecamera (p.es., inserita, assente, notturna) che possono essere attivate su richiesta." }, "timestampPosition": { "tl": "In alto a sinistra", @@ -1564,14 +1833,37 @@ "errorLabel": "Errore", "resultsFields": { "error": "Errore", - "totals": "Totali" + "totals": "Totali", + "filesChecked": "File controllati", + "orphansFound": "Orfani trovati", + "orphansDeleted": "Orfani eliminati", + "aborted": "Interrotto. La cancellazione supererebbe la soglia di sicurezza." }, "event_snapshots": "Istantanee degli oggetti tracciati", "event_thumbnails": "Miniature degli oggetti tracciati", "review_thumbnails": "Anteprima delle miniature", "previews": "Anteprime", "exports": "Esportazioni", - "recordings": "Registrazioni" + "recordings": "Registrazioni", + "mediaTypes": "Tipi di supporto", + "allMedia": "Tutti i supporti", + "dryRun": "Prova a secco", + "dryRunEnabled": "Nessun file verrà eliminato", + "dryRunDisabled": "I file verranno eliminati", + "force": "Forza", + "forceDesc": "Ignora la soglia di sicurezza e completa la sincronizzazione anche se più del 50% dei file verrebbe eliminato.", + "verbose": "Dettagliato", + "verboseDesc": "Scrivi un elenco completo dei file orfani su disco per la revisione.", + "running": "Sincronizzazione in corso...", + "start": "Avvia sincronizzazione", + "inProgress": "Sincronizzazione in corso. Questa pagina è disabilitata.", + "status": { + "queued": "In coda", + "running": "In corso", + "completed": "Completata", + "failed": "Fallita", + "notRunning": "Non in esecuzione" + } }, "regionGrid": { "title": "Griglia di regioni", @@ -1583,5 +1875,158 @@ "clearError": "Impossibile pulire la griglia di regioni", "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia di regioni abbiano effetto" } + }, + "retainMode": { + "motion": "Movimento", + "all": "Tutti", + "active_objects": "Oggetti attivi" + }, + "birdseye": { + "trackingMode": { + "motion": "Movimento", + "objects": "Oggetti", + "continuous": "Continuo" + } + }, + "toast": { + "success": "Impostazioni salvate correttamente", + "applied": "Impostazioni applicate correttamente", + "successRestartRequired": "Impostazioni salvate correttamente. Riavvia Frigate per applicare le modifiche.", + "error": "Impossibile salvare le impostazioni", + "validationError": "Validazione non riuscita: {{message}}", + "resetSuccess": "Ripristina le impostazioni predefinite globali", + "resetError": "Impossibile ripristinare le impostazioni", + "saveAllSuccess_one": "Salvata {{count}} sezione correttamente.", + "saveAllSuccess_many": "Tutte le {{count}} sezioni sono state salvate correttamente.", + "saveAllSuccess_other": "Tutte le {{count}} sezioni sono state salvate correttamente.", + "saveAllPartial_one": "{{successCount}} sezione su {{totalCount}} salvata. {{failCount}} errore.", + "saveAllPartial_many": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.", + "saveAllPartial_other": "{{successCount}} sezioni su {{totalCount}} salvate. {{failCount}} errori.", + "saveAllFailure": "Impossibile salvare tutte le sezioni." + }, + "unsavedChanges": "Hai delle modifiche non salvate", + "confirmReset": "Conferma il ripristino", + "resetToDefaultDescription": "Questa operazione ripristinerà tutte le impostazioni di questa sezione ai valori predefiniti. Tale azione è irreversibile.", + "resetToGlobalDescription": "Questa operazione ripristinerà le impostazioni di questa sezione ai valori predefiniti globali. Tale azione è irreversibile.", + "previewQuality": { + "very_high": "Molto alta", + "high": "Alta", + "medium": "Media", + "low": "Bassa", + "very_low": "Molto bassa" + }, + "ui": { + "TimeOrDateStyle": { + "medium": "Medio", + "full": "Completo", + "long": "Lungo", + "short": "Corto" + }, + "timeFormat": { + "browser": "Navigatore", + "12hour": "12 ore", + "24hour": "24 ore" + }, + "unitSystem": { + "metric": "Metrico", + "imperial": "Imperiale" + } + }, + "review": { + "imageSource": { + "recordings": "Registrazioni", + "previews": "Anteprime" + } + }, + "logger": { + "logLevel": { + "debug": "Correzioni", + "info": "Informazioni", + "warning": "Avviso", + "error": "Errore", + "critical": "Critico" + } + }, + "onvif": { + "profileAuto": "Automatico", + "profileLoading": "Caricamento profili...", + "autotracking": { + "zooming": { + "disabled": "Disabilitato", + "absolute": "Assoluto", + "relative": "Relativo" + } + } + }, + "modelSize": { + "small": "Piccolo", + "large": "Grande" + }, + "configMessages": { + "review": { + "recordDisabled": "La registrazione è disabilitata, pertanto non verranno generati elementi di revisione.", + "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Gli elementi di revisione richiedono la presenza di oggetti rilevati per poter classificare avvisi e rilevamenti.", + "allNonAlertDetections": "Tutte le attività non di avviso saranno incluse tra i rilevamenti.", + "genaiImageSourceRecordingsRecordDisabled": "La sorgente dell'immagine è impostata su 'registrazioni', ma la registrazione è disabilitata. Frigate utilizzerà le immagini di anteprima." + }, + "audio": { + "noAudioRole": "Nessun flusso ha il ruolo audio definito. È necessario abilitare il ruolo audio affinché il rilevamento audio funzioni." + }, + "audioTranscription": { + "audioDetectionDisabled": "Il rilevamento audio non è abilitato per questa telecamera. La trascrizione audio richiede che il rilevamento audio sia attivo." + }, + "detect": { + "fpsGreaterThanFive": "Impostare il valore di FPS rilevato su un valore superiore a 5 non è consigliabile. Valori più elevati potrebbero causare problemi di prestazioni e non apporteranno alcun vantaggio.", + "disabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee, gli elementi di revisione e le funzionalità aggiuntive come il riconoscimento facciale, il riconoscimento delle targhe e l'intelligenza artificiale generativa non funzioneranno." + }, + "objects": { + "genaiNoDescriptionsProvider": "Per generare le descrizioni è necessario configurare un provider GenAI con il ruolo 'descrizioni'." + }, + "faceRecognition": { + "globalDisabled": "Perché le funzionalità di riconoscimento facciale funzionino correttamente su questa telecamera, è necessario abilitare l'arricchimento del riconoscimento facciale.", + "personNotTracked": "Il riconoscimento facciale richiede che l'oggetto 'persona' venga tracciato. Abilita 'persona' nella sezione ogggetti di questa telecamera.", + "modelSizeLarge": "Il modello 'grande' richiede una GPU o una NPU per prestazioni accettabili. Utilizzare il modello 'piccolo' su sistemi dotati solo di CPU." + }, + "lpr": { + "globalDisabled": "Per il corretto funzionamento delle funzioni LPR (riconoscimento targhe) su questa telecamera, è necessario abilitare la funzione di arricchimento del riconoscimento delle targhe.", + "vehicleNotTracked": "Il riconoscimento della targa richiede che venga tracciato 'automobile' o 'moto'. Abilita 'automobile' o 'moto' nella sezione oggetti per questa telecamera.", + "modelSizeLarge": "Il modello 'grande' è ottimizzato per le targhe multilinea. Il modello 'piccolo' offre prestazioni migliori rispetto al modello 'grande' e dovrebbe essere utilizzato a meno che nella vostra regione non siano in vigore formati di targa multilinea." + }, + "record": { + "noRecordRole": "Nessun flusso ha il ruolo di registrazione definito. La registrazione non funzionerà." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye è impostato sulla modalità 'oggetti', ma il rilevamento degli oggetti è disabilitato per questa telecamera. La telecamera non verrà visualizzata in Birdseye." + }, + "snapshots": { + "detectDisabled": "Il rilevamento degli oggetti è disabilitato. Le istantanee vengono generate dagli oggetti tracciati e non verranno create." + }, + "detectors": { + "mixedTypes": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti per poter utilizzare un tipo diverso.", + "mixedTypesSuggestion": "Tutti i rilevatori devono essere dello stesso tipo. Rimuovi i rilevatori esistenti oppure seleziona {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Il modello 'piccolo' Jina V2 presenta elevati consumi di RAM e di inferenza. Si consiglia il modello 'grande' con GPU dedicata." + } + }, + "saveAllPreview": { + "title": "Modifiche da salvare", + "triggerLabel": "Revisione delle modifiche in sospeso", + "empty": "Nessuna modifica in sospeso.", + "scope": { + "label": "Ambito", + "global": "Globale", + "camera": "Telecamera: {{cameraName}}" + }, + "profile": { + "label": "Profilo" + }, + "field": { + "label": "Campo" + }, + "value": { + "label": "Nuovo valore", + "reset": "Reimposta" + } } } diff --git a/web/public/locales/it/views/system.json b/web/public/locales/it/views/system.json index ca6a0ab9ce..ed780a51e9 100644 --- a/web/public/locales/it/views/system.json +++ b/web/public/locales/it/views/system.json @@ -175,7 +175,7 @@ "framesAndDetections": "Fotogrammi / Rilevamenti", "label": { "camera": "telecamera", - "detect": "rilevamento", + "detect": "rileva", "skipped": "saltati", "ffmpeg": "FFmpeg", "capture": "cattura", From 59faa4e0880a3c61583a35abd89ba87c6bf3b895 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:53 +0200 Subject: [PATCH 20/94] Translated using Weblate (Dutch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Dutch) Currently translated at 83.0% (49 of 59 strings) Translated using Weblate (Dutch) Currently translated at 83.9% (397 of 473 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (1150 of 1150 strings) Translated using Weblate (Dutch) Currently translated at 15.2% (72 of 473 strings) Translated using Weblate (Dutch) Currently translated at 30.0% (15 of 50 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (45 of 45 strings) Translated using Weblate (Dutch) Currently translated at 10.2% (81 of 794 strings) Translated using Weblate (Dutch) Currently translated at 59.3% (35 of 59 strings) Translated using Weblate (Dutch) Currently translated at 35.0% (14 of 40 strings) Translated using Weblate (Dutch) Currently translated at 23.7% (14 of 59 strings) Translated using Weblate (Dutch) Currently translated at 24.4% (11 of 45 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (26 of 26 strings) Translated using Weblate (Dutch) Currently translated at 63.4% (712 of 1122 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (25 of 25 strings) Translated using Weblate (Dutch) Currently translated at 11.8% (7 of 59 strings) Translated using Weblate (Dutch) Currently translated at 20.0% (8 of 40 strings) Translated using Weblate (Dutch) Currently translated at 8.8% (4 of 45 strings) Translated using Weblate (Dutch) Currently translated at 10.1% (80 of 792 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (22 of 22 strings) Translated using Weblate (Dutch) Currently translated at 93.7% (121 of 129 strings) Co-authored-by: Bart Smeding Co-authored-by: Björn Vanneste Co-authored-by: Hosted Weblate Co-authored-by: Hosted Weblate user 151476 Co-authored-by: bb61523 Co-authored-by: soosterwaal Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/nl/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation Translation: Frigate NVR/components-player Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-classificationmodel Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-settings --- web/public/locales/nl/components/player.json | 3 +- web/public/locales/nl/config/cameras.json | 702 +++++++- web/public/locales/nl/config/global.json | 1509 ++++++++++++++++- web/public/locales/nl/config/groups.json | 2 +- web/public/locales/nl/config/validation.json | 2 +- web/public/locales/nl/views/chat.json | 18 +- .../locales/nl/views/classificationModel.json | 8 +- web/public/locales/nl/views/motionSearch.json | 66 +- web/public/locales/nl/views/replay.json | 60 +- web/public/locales/nl/views/settings.json | 725 +++++++- 10 files changed, 2997 insertions(+), 98 deletions(-) diff --git a/web/public/locales/nl/components/player.json b/web/public/locales/nl/components/player.json index ff0dd10655..7ec53a0f1f 100644 --- a/web/public/locales/nl/components/player.json +++ b/web/public/locales/nl/components/player.json @@ -30,7 +30,8 @@ }, "submitFrigatePlus": { "title": "Dit frame indienen bij Frigate+?", - "submit": "Indienen" + "submit": "Indienen", + "previewError": "Het was niet mogelijk om de snapshot preview te laden. De opname is mogelijk niet beschikbaar op dit moment." }, "streamOffline": { "title": "Stream is Offline", diff --git a/web/public/locales/nl/config/cameras.json b/web/public/locales/nl/config/cameras.json index 96b78e382f..a70df21343 100644 --- a/web/public/locales/nl/config/cameras.json +++ b/web/public/locales/nl/config/cameras.json @@ -13,7 +13,7 @@ "description": "Geactiveerd" }, "audio": { - "label": "Audiogebeurtenissen", + "label": "Geluiddetectie", "description": "Audio-instellingen voor gebeurtenisdetectie van deze camera.", "enabled": { "label": "Geluiddetectie inschakelen", @@ -21,19 +21,23 @@ }, "max_not_heard": { "label": "Einde timeout", - "description": "Hoeveelheid secondes zonder de geconfigureerde audio soort, voordat de geluids gebeurtenis is beindigd." + "description": "Aantal seconden zonder het geconfigureerde audiotype, voordat de geluidsgebeurtenis is beëindigd." }, "min_volume": { - "label": "Minimale volume", - "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; Hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." + "label": "Minimumvolume", + "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." }, "listen": { "label": "Luistercategorieën", "description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)." }, "filters": { - "label": "Geluids filters", - "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties." + "label": "Geluidsfilters", + "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.", + "threshold": { + "label": "Minimale audiobetrouwbaarheid", + "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld." + } }, "enabled_in_config": { "label": "Originele audio-instelling", @@ -45,7 +49,7 @@ } }, "audio_transcription": { - "label": "Audio‑transcriptie", + "label": "Audiotranscriptie", "description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.", "enabled": { "label": "Spraaktranscriptie inschakelen", @@ -60,14 +64,14 @@ } }, "birdseye": { - "label": "Overzichtsweergave", + "label": "Birdseye-overzicht", "description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.", "enabled": { - "label": "Activeer overzichtsweergave", + "label": "Birdseye-overzicht inschakelen", "description": "De overzichtsweergavefunctie in- of uitschakelen." }, "mode": { - "label": "Volgmodus", + "label": "Weergavemodus", "description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’." }, "order": { @@ -76,18 +80,18 @@ } }, "detect": { - "label": "Detectie object", + "label": "Objectdetectie", "description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.", "enabled": { - "label": "Detectie aan", + "label": "Detectie inschakelen", "description": "Objectdetectie voor deze camera in- of uitschakelen. Detectie moet zijn ingeschakeld om objecttracking te laten werken." }, "height": { - "label": "Detectie hoogte", + "label": "Detectiehoogte", "description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." }, "width": { - "label": "Detectie breedte", + "label": "Detectiebreedte", "description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." }, "fps": { @@ -121,10 +125,18 @@ "description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt." }, "objects": { - "label": "Object‑maximum aantal frames", - "description": "Per‑object overschrijden voor het maximum aantal frames voor tracking van stationaire objecten." + "label": "Maximaal aantal frames per object", + "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten." } + }, + "classifier": { + "label": "Visuele classifier inschakelen", + "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven." } + }, + "annotation_offset": { + "label": "Annotatie-offset", + "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn." } }, "profiles": { @@ -148,5 +160,663 @@ "label": "Minimale oppervlakte van het object" } } + }, + "mqtt": { + "label": "MQTT" + }, + "notifications": { + "label": "Meldingen", + "enabled": { + "label": "Meldingen inschakelen" + }, + "email": { + "label": "Melding email", + "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders." + }, + "cooldown": { + "label": "Wachttijd", + "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.", + "path": { + "label": "FFmpeg-pad", + "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "Globale argumenten voor FFmpeg-processen." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten voor FFmpeg-invoerstromen." + }, + "output_args": { + "label": "Uitvoerargumenten", + "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.", + "detect": { + "label": "Uitvoerargumenten voor detectie", + "description": "Standaard uitvoerargumenten voor streams met detectierol." + }, + "record": { + "label": "Uitvoerargumenten voor opname", + "description": "Standaard uitvoerargumenten voor streams met opnamerol." + } + }, + "retry_interval": { + "label": "FFmpeg-herverbindingstijd", + "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10." + }, + "apple_compatibility": { + "label": "Apple-compatibiliteit", + "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265." + }, + "gpu": { + "label": "GPU-index", + "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar." + }, + "inputs": { + "label": "Camera-invoer", + "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.", + "path": { + "label": "Invoerpad", + "description": "URL of pad van de camera-invoerstroom." + }, + "roles": { + "label": "Invoerrollen", + "description": "Rollen voor deze invoerstroom." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "FFmpeg globale argumenten voor deze invoerstroom." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor deze invoerstroom." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten specifiek voor deze stream." + } + } + }, + "live": { + "label": "Live weergave", + "streams": { + "label": "Live streamnamen", + "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave." + }, + "height": { + "label": "Live hoogte", + "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn." + }, + "quality": { + "label": "Live kwaliteit", + "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)." + } + }, + "motion": { + "label": "Bewegingsdetectie", + "enabled": { + "label": "Bewegingsdetectie inschakelen" + }, + "threshold": { + "label": "Bewegingsdrempel", + "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)." + }, + "lightning_threshold": { + "label": "Bliksemdrempel", + "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt." + }, + "skip_motion_threshold": { + "label": "Drempel voor overgeslagen beweging", + "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen." + }, + "improve_contrast": { + "label": "Contrast verbeteren", + "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren." + }, + "contour_area": { + "label": "Contouroppervlakte", + "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld." + }, + "delta_alpha": { + "label": "Delta-alfa", + "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening." + }, + "frame_alpha": { + "label": "Frame-alfa", + "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking." + }, + "frame_height": { + "label": "Framehoogte", + "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging." + }, + "mask": { + "label": "Maskercoördinaten", + "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden." + }, + "mqtt_off_delay": { + "label": "MQTT uit-vertraging", + "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status." + }, + "enabled_in_config": { + "label": "Originele bewegingsstatus", + "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "objects": { + "label": "Objecten", + "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.", + "track": { + "label": "Te volgen objecten" + }, + "filters": { + "label": "Objectfilters", + "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).", + "min_area": { + "label": "Minimale objectoppervlakte", + "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "max_area": { + "label": "Maximale objectoppervlakte", + "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "min_ratio": { + "label": "Minimale beeldverhouding", + "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "max_ratio": { + "label": "Maximale beeldverhouding", + "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "threshold": { + "label": "Betrouwbaarheidsdrempel", + "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen." + }, + "min_score": { + "label": "Minimale betrouwbaarheid", + "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen." + }, + "mask": { + "label": "Filtermasker", + "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "mask": { + "label": "Objectmasker", + "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen." + }, + "raw_mask": { + "label": "Onbewerkt masker" + }, + "genai": { + "label": "GenAI-objectconfiguratie", + "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.", + "enabled": { + "label": "GenAI inschakelen", + "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen." + }, + "use_snapshot": { + "label": "Snapshots gebruiken", + "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving." + }, + "prompt": { + "label": "Bijschriftprompt", + "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI." + }, + "object_prompts": { + "label": "Objectprompts", + "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels." + }, + "objects": { + "label": "GenAI-objecten", + "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing." + }, + "send_triggers": { + "label": "GenAI-triggers", + "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).", + "tracked_object_end": { + "label": "Sturen bij beëindiging", + "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt." + }, + "after_significant_updates": { + "label": "Vroege GenAI-trigger", + "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object." + } + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie." + } + } + }, + "record": { + "label": "Opname", + "enabled": { + "label": "Opname inschakelen" + }, + "expire_interval": { + "label": "Opruiminterval opnames", + "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen." + }, + "continuous": { + "label": "Continue bewaring", + "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "motion": { + "label": "Bewegingsretentie", + "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "detections": { + "label": "Detectieretentie", + "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "alerts": { + "label": "Meldingsbewaring", + "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "export": { + "label": "Exportconfiguratie", + "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.", + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten voor export", + "description": "Hardwareversnellingsargumenten voor export/transcodering." + }, + "max_concurrent": { + "label": "Maximaal aantal gelijktijdige exports", + "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt." + } + }, + "preview": { + "label": "Voorbeeldconfiguratie", + "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.", + "quality": { + "label": "Voorbeeldkwaliteit", + "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Originele opnamestatus", + "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie." + } + }, + "review": { + "label": "Beoordeling", + "alerts": { + "label": "Meldingsconfiguratie", + "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.", + "enabled": { + "label": "Alerts inschakelen" + }, + "labels": { + "label": "Meldingslabels", + "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie." + }, + "cutoff_time": { + "label": "Afsluitingstijd meldingen", + "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten." + } + }, + "detections": { + "label": "Detectieconfiguratie", + "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.", + "enabled": { + "label": "Detecties inschakelen" + }, + "labels": { + "label": "Detectielabels", + "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone." + }, + "cutoff_time": { + "label": "Afsluitingstijd detecties", + "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten." + }, + "enabled_in_config": { + "label": "Originele detectiestatus", + "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie." + } + }, + "genai": { + "label": "GenAI-configuratie", + "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.", + "enabled": { + "label": "GenAI-beschrijvingen inschakelen", + "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen." + }, + "alerts": { + "label": "GenAI inschakelen voor meldingen", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems." + }, + "detections": { + "label": "GenAI inschakelen voor detecties", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen." + }, + "image_source": { + "label": "Afbeeldingsbron voor beoordeling", + "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens." + }, + "additional_concerns": { + "label": "Aanvullende aandachtspunten", + "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing." + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie." + }, + "preferred_language": { + "label": "Voorkeurstaal", + "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider." + }, + "activity_context_prompt": { + "label": "Activiteitscontextprompt", + "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen." + } + } + }, + "snapshots": { + "label": "Snapshots", + "enabled": { + "label": "Snapshots inschakelen" + }, + "timestamp": { + "label": "Tijdstempel-overlay", + "description": "Een tijdstempel op API-snapshots weergeven." + }, + "bounding_box": { + "label": "Detectiekader-overlay", + "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots." + }, + "crop": { + "label": "Snapshot bijsnijden", + "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen." + }, + "height": { + "label": "Snapshothoogte", + "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden." + }, + "retain": { + "label": "Snapshot-bewaring", + "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.", + "default": { + "label": "Standaard retentie", + "description": "Standaard aantal dagen om snapshots te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + }, + "objects": { + "label": "Objectbewaring", + "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots." + } + }, + "quality": { + "label": "Snapshotkwaliteit", + "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Tijdstempelstijl", + "position": { + "label": "Tijdstempelpositie", + "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)." + }, + "format": { + "label": "Tijdstempelformaat", + "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)." + }, + "color": { + "label": "Tijdstempelkleur", + "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).", + "red": { + "label": "Rood", + "description": "Roodcomponent (0-255) voor de tijdstempelkleur." + }, + "green": { + "label": "Groen", + "description": "Groencomponent (0-255) voor de tijdstempelkleur." + }, + "blue": { + "label": "Blauw", + "description": "Blauwcomponent (0-255) voor de tijdstempelkleur." + } + }, + "thickness": { + "label": "Tijdstempeldikte", + "description": "Lijndikte van de tijdstempeltekst." + }, + "effect": { + "label": "Tijdstempeleffect", + "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)." + } + }, + "semantic_search": { + "label": "Semantisch zoeken", + "triggers": { + "label": "Triggers", + "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Optionele weergavenaam voor deze trigger in de UI." + }, + "enabled": { + "label": "Trigger inschakelen", + "description": "Deze semantisch-zoeken-trigger in- of uitschakelen." + }, + "type": { + "label": "Triggertype", + "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)." + }, + "data": { + "label": "Triggerinhoud", + "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten." + }, + "threshold": { + "label": "Triggerdrempel", + "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren." + }, + "actions": { + "label": "Triggeracties", + "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)." + } + } + }, + "face_recognition": { + "label": "Gezichtsherkenning", + "enabled": { + "label": "Gezichtsherkenning inschakelen" + }, + "min_area": { + "label": "Minimale gezichtsoppervlakte", + "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen." + } + }, + "lpr": { + "label": "Kentekenherkenning", + "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.", + "enabled": { + "label": "LPR inschakelen" + }, + "min_area": { + "label": "Minimale kentekenoppervlakte", + "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen." + }, + "enhancement": { + "label": "Verbeteringsniveau", + "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt." + }, + "expire_time": { + "label": "Vervaltijd in seconden", + "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.", + "host": { + "label": "ONVIF-host", + "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera." + }, + "port": { + "label": "ONVIF-poort", + "description": "Poortnummer voor de ONVIF-dienst." + }, + "user": { + "label": "ONVIF-gebruikersnaam", + "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF." + }, + "password": { + "label": "ONVIF-wachtwoord", + "description": "Wachtwoord voor ONVIF-authenticatie." + }, + "tls_insecure": { + "label": "TLS-verificatie uitschakelen", + "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)." + }, + "profile": { + "label": "ONVIF-profiel", + "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd." + }, + "autotracking": { + "label": "Automatisch volgen", + "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.", + "enabled": { + "label": "Automatisch volgen inschakelen", + "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen." + }, + "calibrate_on_startup": { + "label": "Kalibreren bij opstarten", + "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie." + }, + "zooming": { + "label": "Zoommodus", + "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoomfactor", + "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75." + }, + "track": { + "label": "Gevolgde objecten", + "description": "Lijst van objecttypen die automatisch volgen activeren." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint." + }, + "return_preset": { + "label": "Terugkeer-voorinstelling", + "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen." + }, + "timeout": { + "label": "Terugkeertimeout", + "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert." + }, + "movement_weights": { + "label": "Bewegingsgewichten", + "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen." + }, + "enabled_in_config": { + "label": "Originele autovolgstatus", + "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie." + } + }, + "ignore_time_mismatch": { + "label": "Tijdsverschil negeren", + "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie." + } } } diff --git a/web/public/locales/nl/config/global.json b/web/public/locales/nl/config/global.json index adc9aa42d9..8943539c8a 100644 --- a/web/public/locales/nl/config/global.json +++ b/web/public/locales/nl/config/global.json @@ -1,24 +1,29 @@ { "audio": { - "label": "Audiogebeurtenissen", + "label": "Geluiddetectie", "enabled": { - "label": "Geluiddetectie inschakelen" + "label": "Geluiddetectie inschakelen", + "description": "Audioeventdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." }, "max_not_heard": { "label": "Einde timeout", - "description": "Hoeveelheid secondes zonder de geconfigureerde audio soort, voordat de geluids gebeurtenis is beindigd." + "description": "Aantal seconden zonder het geconfigureerde audiotype, voordat de geluidsgebeurtenis is beëindigd." }, "min_volume": { - "label": "Minimale volume", - "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; Hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." + "label": "Minimumvolume", + "description": "Minimale RMS-volumedrempel die nodig is om audiodetectie te starten; hoe lager de waarde, hoe gevoeliger de detectie (bijvoorbeeld, 200 hoog, 500 gemiddeld, 1000 laag)." }, "listen": { "label": "Luistercategorieën", "description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, schreeuw, praten, roepen)." }, "filters": { - "label": "Geluids filters", - "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties." + "label": "Geluidsfilters", + "description": "Instellingen per audiotype, waaronder betrouwbaarheidsdrempels, ter vermindering van foutieve detecties.", + "threshold": { + "label": "Minimale audiobetrouwbaarheid", + "description": "Minimale betrouwbaarheidsdrempel voor de audiogebeurtenis om te worden geteld." + } }, "enabled_in_config": { "label": "Originele audio-instelling", @@ -27,44 +32,98 @@ "num_threads": { "label": "Detectiethreads", "description": "Aantal threads voor audiodetectieverwerking." - } + }, + "description": "Instellingen voor audiogebaseerde gebeurtenisdetectie voor alle camera's; kan per camera worden overschreven." }, "audio_transcription": { - "label": "Audio‑transcriptie", + "label": "Audiotranscriptie", "description": "Instellingen voor live en spraakgestuurde audiotranscriptie voor gebeurtenissen en live ondertitels.", "live_enabled": { "label": "Live transcriptie", "description": "Live streaming‑transcriptie van audio inschakelen tijdens ontvangst." + }, + "enabled": { + "label": "Audiotranscriptie inschakelen", + "description": "Automatische audiotranscriptie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "language": { + "label": "Transcriptietaal", + "description": "Taalcode voor transcriptie/vertaling (bijv. 'nl' voor Nederlands). Zie https://whisper-api.com/docs/languages/ voor ondersteunde taalcodes." + }, + "device": { + "label": "Transcriptieapparaat", + "description": "Apparaat (CPU/GPU) voor het uitvoeren van het transcriptiemodel. Momenteel worden alleen NVIDIA CUDA GPU's ondersteund voor transcriptie." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor offline audiotranscriptie." } }, "birdseye": { - "label": "Overzichtsweergave", + "label": "Birdseye-overzicht", "description": "Instellingen voor de overzichtsweergave die meerdere camerafeeds combineert tot één lay‑out.", "enabled": { - "label": "Activeer overzichtsweergave", + "label": "Birdseye-overzicht inschakelen", "description": "De overzichtsweergavefunctie in- of uitschakelen." }, "mode": { - "label": "Volgmodus", + "label": "Weergavemodus", "description": "Modus voor het opnemen van camera’s in overzichtsweergave: ‘objecten’, ‘beweging’ of ‘continu’." }, "order": { "label": "Positie", "description": "Numerieke positie die de volgorde van de camera in de overzichtsweergave lay-out bepaalt." + }, + "restream": { + "label": "RTSP-herstreaming", + "description": "De Birdseye-uitvoer herstreamen als RTSP-feed; hierdoor blijft Birdseye continu actief." + }, + "width": { + "label": "Breedte", + "description": "Uitvoerbreedte (pixels) van het samengestelde Birdseye-frame." + }, + "height": { + "label": "Hoogte", + "description": "Uitvoerhoogte (pixels) van het samengestelde Birdseye-frame." + }, + "quality": { + "label": "Coderingskwaliteit", + "description": "Coderingskwaliteit van de Birdseye MPEG-1-feed (1 = hoogste kwaliteit, 31 = laagste)." + }, + "inactivity_threshold": { + "label": "Inactiviteitsdrempel", + "description": "Seconden inactiviteit waarna een camera niet meer in Birdseye wordt getoond." + }, + "layout": { + "label": "Lay-out", + "description": "Lay-outopties voor de Birdseye-samenstelling.", + "scaling_factor": { + "label": "Schaalfactor", + "description": "Schaalfactor voor de lay-outcalculator (bereik 1,0 tot 5,0)." + }, + "max_cameras": { + "label": "Maximum camera's", + "description": "Maximaal aantal camera's dat tegelijk in Birdseye wordt weergegeven; toont de meest recente camera's." + } + }, + "idle_heartbeat_fps": { + "label": "Inactief heartbeat-FPS", + "description": "Frames per seconde voor het opnieuw verzenden van het laatste Birdseye-frame tijdens inactiviteit; stel 0 in om uit te schakelen." } }, "detect": { - "label": "Detectie object", + "label": "Objectdetectie", "description": "Instellingen voor de detectierol om objecten te detecteren en trackers te starten.", "enabled": { - "label": "Detectie aan" + "label": "Detectie inschakelen", + "description": "Objectdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." }, "height": { - "label": "Detectie hoogte", + "label": "Detectiehoogte", "description": "De hoogte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." }, "width": { - "label": "Detectie breedte", + "label": "Detectiebreedte", "description": "De breedte in pixels van frames voor de detectiestream. Laat dit veld leeg om de standaardresolutie te gebruiken." }, "fps": { @@ -98,72 +157,1444 @@ "description": "Standaardlimiet voor het aantal frames dat een stilstaand object wordt gevolgd voordat wordt gestopt." }, "objects": { - "label": "Object‑maximum aantal frames", - "description": "Per‑object overschrijden voor het maximum aantal frames voor tracking van stationaire objecten." + "label": "Maximaal aantal frames per object", + "description": "Maximum aantal frames per object bij het volgen van stilstaande objecten." } + }, + "classifier": { + "label": "Visuele classifier inschakelen", + "description": "Gebruik een visuele classifier om echt stilstaande objecten te detecteren, zelfs wanneer detectiekaders licht verschuiven." } + }, + "annotation_offset": { + "label": "Annotatie-offset", + "description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn." } }, "version": { "description": "Numerieke of string-versie van de actieve configuratie om migraties of formaatwijzigingen te helpen detecteren.", - "label": "Huidige configuratie versie" + "label": "Huidige config-versie" }, "safe_mode": { "label": "Veilige modus", - "description": "Wanneer ingeschakeld, start Frigate in veilige modus met verminderde functionaliteit voor probleemoplossing." + "description": "Wanneer ingeschakeld, start Frigate op in veilige modus met beperkte functies voor probleemoplossing." }, "environment_vars": { "label": "Omgevingsvariabelen", - "description": "Sleutel/waarde paren van omgevingsvariabelen voor het Frigate proces in Home Assistant OS. Niet-HAOS gebruikers moeten in plaats hiervan Docker omgevingsvariabelen gebruiken." + "description": "Sleutel/waarde-paren van omgevingsvariabelen die ingesteld worden voor het Frigate-proces in Home Assistant OS. Gebruikers zonder HAOS moeten in plaats daarvan de Docker-omgevingsvariabelenconfiguratie gebruiken." }, "auth": { "label": "Authenticatie", "enabled": { - "label": "Authenticatie aanzetten", + "label": "Authenticatie inschakelen", "description": "Schakel native authenticatie in voor de Frigate UI." }, "reset_admin_password": { - "label": "Reset admin wachtwoord", - "description": "Indien waar, reset het admin gebruiker wachtwoord tijdens opstarten en print het nieuwe wachtwoord in het logboek." + "label": "Adminwachtwoord resetten", + "description": "Indien waar, reset het wachtwoord van de admingebruiker tijdens opstarten en print het nieuwe wachtwoord in het logboek." }, - "description": "Authenticatie en sessie-gerelateerde instellingen inclusief cookie en tempo limiet opties.", + "description": "Authenticatie- en sessie-instellingen inclusief cookie- en snelheidsbeperkingsopties.", "cookie_name": { - "label": "JWT cookie naam", + "label": "JWT-cookienaam", "description": "Naam van de gebruikte cookie om de JWT token voor native authenticatie op te slaan." }, "cookie_secure": { - "label": "Veilige cookie instelling", + "label": "Secure-cookievlag", "description": "Stel de veilige instelling in op de auth cookie; moet waar zijn indien TLS in gebruik." }, "session_length": { - "label": "Sessie duratie", - "description": "Sessie duratie in seconden voor JWT-gebaseerde sessies." + "label": "Sessieduur", + "description": "Sessieduur in seconden voor JWT-gebaseerde sessies." }, "refresh_time": { - "label": "Sessie ververs scherm", - "description": "Als een sessie binnen dit aantal seconden verloopt, ververs het tot volledige duratie." + "label": "Sessie-verversperiode", + "description": "Als een sessie binnen dit aantal seconden verloopt, wordt de sessie verlengd tot de volledige duur." }, "failed_login_rate_limit": { - "label": "Gefaalde log-in pogingen", - "description": "Tempo-limiet regels voor gefaalde inlogpogingen om brute-force aanvallen te beperken." + "label": "Limieten voor mislukte inlogpogingen", + "description": "Rate-limitregels voor mislukte inlogpogingen om brute-forceaanvallen te beperken." }, "trusted_proxies": { - "label": "Vertrouwde proxies" + "label": "Vertrouwde proxies", + "description": "Lijst met vertrouwde proxy-IP's die worden gebruikt bij het bepalen van het client-IP voor rate limiting." + }, + "hash_iterations": { + "label": "Hash-iteraties", + "description": "Aantal PBKDF2-SHA256-iteraties voor het hashen van gebruikerswachtwoorden." + }, + "roles": { + "label": "Roltoewijzingen", + "description": "Koppel rollen aan cameralijsten. Een lege lijst geeft de rol toegang tot alle camera's." + }, + "admin_first_time_login": { + "label": "Eerste keer admin-vlag", + "description": "Wanneer ingeschakeld kan de UI een helplink tonen op de inlogpagina om gebruikers te informeren hoe ze kunnen inloggen na een admin-wachtwoordreset. " } }, "logger": { "default": { "label": "Loggingsniveau", - "description": "Standaard globale logboek detailniveau (debug, info, waarschuwing, fout)." + "description": "Standaard globale logdetailniveau (debug, info, warning, error)." }, "label": "Logging", "logs": { - "label": "Per-proces logboek niveau", - "description": "Per-component logboekniveau afwijkingen om detailniveau te vergroten of verkleinen per specifieke module." + "label": "Logboekniveau per proces", + "description": "Logboekniveau-afwijkingen per component om het detailniveau per specifieke module te verhogen of verlagen." }, - "description": "Beheert het standaard logboek detailniveau en afwijkende instellingen per logboek." + "description": "Beheert het standaard logdetailniveau en afwijkende instellingen per logboek." }, "profiles": { - "label": "Profielen" + "label": "Profielen", + "description": "Benoemde profieldefinities met weergavenamen. Cameraprofielen moeten verwijzen naar hier gedefinieerde namen.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Weergavenaam voor dit profiel in de UI." + } + }, + "database": { + "label": "Database", + "description": "Instellingen voor de SQLite-database die Frigate gebruikt om gevolgde objecten en opname-metadata op te slaan.", + "path": { + "label": "Databasepad", + "description": "Bestandssysteempad waar het Frigate SQLite-databasebestand wordt opgeslagen." + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "Instellingen voor de geïntegreerde go2rtc-restreaming-service voor het doorzenden en omzetten van live streams." + }, + "mqtt": { + "label": "MQTT", + "description": "Instellingen voor het verbinden met en publiceren van telemetrie, snapshots en gebeurtenisdetails naar een MQTT-broker.", + "enabled": { + "label": "MQTT inschakelen", + "description": "MQTT-integratie voor status, gebeurtenissen en snapshots in- of uitschakelen." + }, + "host": { + "label": "MQTT-host", + "description": "Hostnaam of IP-adres van de MQTT-broker." + }, + "port": { + "label": "MQTT-poort", + "description": "Poort van de MQTT-broker (gewoonlijk 1883 voor gewoon MQTT)." + }, + "topic_prefix": { + "label": "Topic-prefix", + "description": "MQTT-topic-prefix voor alle Frigate-topics; moet uniek zijn bij meerdere instanties." + }, + "client_id": { + "label": "Client-ID", + "description": "Client-ID voor verbinding met de MQTT-broker; moet uniek zijn per instantie." + }, + "stats_interval": { + "label": "Statistiekeninterval", + "description": "Interval in seconden voor het publiceren van systeem- en camerastatistieken naar MQTT." + }, + "user": { + "label": "MQTT-gebruikersnaam", + "description": "Optionele MQTT-gebruikersnaam; kan via omgevingsvariabelen of secrets worden opgegeven." + }, + "password": { + "label": "MQTT-wachtwoord", + "description": "Optioneel MQTT-wachtwoord; kan via omgevingsvariabelen of secrets worden opgegeven." + }, + "tls_ca_certs": { + "label": "TLS CA-certificaten", + "description": "Pad naar het CA-certificaat voor TLS-verbindingen met de broker (voor zelfondertekende certificaten)." + }, + "tls_client_cert": { + "label": "Clientcertificaat", + "description": "Pad naar het clientcertificaat voor wederzijdse TLS-authenticatie; stel geen gebruiker/wachtwoord in bij gebruik van clientcertificaten." + }, + "tls_client_key": { + "label": "Clientsleutel", + "description": "Pad naar de privésleutel van het clientcertificaat." + }, + "tls_insecure": { + "label": "Onveilige TLS", + "description": "Onveilige TLS-verbindingen toestaan door hostnaamverificatie over te slaan (niet aanbevolen)." + }, + "qos": { + "label": "MQTT QoS-niveau", + "description": "QoS-niveau voor MQTT-publicaties/abonnementen (0, 1 of 2)." + } + }, + "notifications": { + "label": "Meldingen", + "description": "Instellingen om meldingen voor alle camera's in te schakelen en te beheren; kan per camera worden overschreven.", + "enabled": { + "label": "Meldingen inschakelen", + "description": "Meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "email": { + "label": "Melding email", + "description": "E-mailadres voor pushmeldingen of vereist door bepaalde meldingsproviders." + }, + "cooldown": { + "label": "Wachttijd", + "description": "Wachttijd (seconden) tussen meldingen om spammen te voorkomen." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie." + } + }, + "networking": { + "label": "Netwerken", + "description": "Netwerkinstellingen zoals IPv6-ondersteuning voor Frigate-eindpunten.", + "ipv6": { + "label": "IPv6-configuratie", + "description": "IPv6-instellingen voor Frigate-netwerkdiensten.", + "enabled": { + "label": "IPv6 inschakelen", + "description": "IPv6-ondersteuning voor Frigate-diensten (API en UI) inschakelen waar van toepassing." + } + }, + "listen": { + "label": "Luisterpoortenconfiguratie", + "description": "Configuratie van interne en externe luisterpoorten. Dit is voor gevorderde gebruikers. In de meeste gevallen wordt aanbevolen de poortensectie in het Docker Compose-bestand aan te passen.", + "internal": { + "label": "Interne poort", + "description": "Interne luisterpoort voor Frigate (standaard 5000)." + }, + "external": { + "label": "Externe poort", + "description": "Externe luisterpoort voor Frigate (standaard 8971)." + } + } + }, + "proxy": { + "label": "Proxy", + "description": "Instellingen voor het integreren van Frigate achter een reverse proxy die geauthenticeerde gebruikersheaders doorgeeft.", + "header_map": { + "label": "Headertoewijzing", + "description": "Inkomende proxyheaders koppelen aan Frigate gebruikers- en rolvelden voor proxy-authenticatie.", + "user": { + "label": "Gebruikersheader", + "description": "Header met de geauthenticeerde gebruikersnaam van de upstream-proxy." + }, + "role": { + "label": "Rolheader", + "description": "Header met de rol of groepen van de geauthenticeerde gebruiker van de upstream-proxy." + }, + "role_map": { + "label": "Roltoewijzing", + "description": "Koppel upstream-groepswaarden aan Frigate-rollen (bijv. admingroepen aan de adminrol)." + } + }, + "logout_url": { + "label": "Uitlog-URL", + "description": "URL waarnaar gebruikers worden doorgestuurd bij uitloggen via de proxy." + }, + "auth_secret": { + "label": "Proxygeheim", + "description": "Optioneel geheim dat wordt gecontroleerd tegen de X-Proxy-Secret-header om vertrouwde proxies te verifiëren." + }, + "default_role": { + "label": "Standaardrol", + "description": "Standaardrol toegewezen aan proxy-geauthenticeerde gebruikers wanneer geen roltoewijzing van toepassing is (admin of viewer)." + }, + "separator": { + "label": "Scheidingsteken", + "description": "Scheidingsteken voor meerdere waarden in proxyheaders." + } + }, + "telemetry": { + "label": "Telemetrie", + "description": "Opties voor systeemtelemetrie en statistieken, inclusief GPU- en netwerkbandbreedtebewaking.", + "network_interfaces": { + "label": "Netwerkinterfaces", + "description": "Lijst met netwerkinterfacenaamprefixen voor bandbreedtestatistieken." + }, + "stats": { + "label": "Systeemstatistieken", + "description": "Opties voor het in- of uitschakelen van het verzamelen van systeem- en GPU-statistieken.", + "amd_gpu_stats": { + "label": "AMD GPU-statistieken", + "description": "Verzameling van AMD GPU-statistieken inschakelen indien een AMD GPU aanwezig is." + }, + "intel_gpu_stats": { + "label": "Intel GPU-statistieken", + "description": "Verzameling van Intel GPU-statistieken inschakelen indien een Intel GPU aanwezig is." + }, + "network_bandwidth": { + "label": "Netwerkbandbreedte", + "description": "Per-proces netwerkbandbreedtebewaking voor camera-ffmpeg-processen en detectoren inschakelen (vereist Linux-capabilities)." + }, + "intel_gpu_device": { + "label": "Intel GPU-apparaat", + "description": "PCI-busadres of DRM-apparaatpad (bijv. /dev/dri/card1) om Intel GPU-statistieken aan een specifiek apparaat te koppelen bij meerdere GPU's." + } + }, + "version_check": { + "label": "Versiecontrole", + "description": "Een uitgaande controle inschakelen om te detecteren of een nieuwere Frigate-versie beschikbaar is." + } + }, + "tls": { + "label": "TLS", + "description": "TLS-instellingen voor de Frigate-webservice (poort 8971).", + "enabled": { + "label": "TLS inschakelen", + "description": "TLS inschakelen voor de Frigate-webinterface en API op de geconfigureerde TLS-poort." + } + }, + "ui": { + "label": "UI", + "description": "Gebruikersinterfacevoorkeuren zoals tijdzone, tijd/datumopmaak en eenheden.", + "timezone": { + "label": "Tijdzone", + "description": "Optionele tijdzone voor weergave in de UI (standaard browsertijd indien niet ingesteld)." + }, + "time_format": { + "label": "Tijdnotatie", + "description": "Tijdnotatie voor de UI (browser, 12-uurs of 24-uurs)." + }, + "date_style": { + "label": "Datumstijl", + "description": "Datumstijl voor de UI (vol, lang, middel, kort)." + }, + "time_style": { + "label": "Tijdstijl", + "description": "Tijdstijl voor de UI (vol, lang, middel, kort)." + }, + "unit_system": { + "label": "Eenhedensysteem", + "description": "Eenhedensysteem voor weergave (metrisch of imperiaal) in de UI en MQTT." + } + }, + "detectors": { + "label": "Detector hardware", + "description": "Configuratie voor objectdetectors (CPU, GPU, ONNX-backends) en detector-specifieke modelinstellingen.", + "type": { + "label": "Type" + }, + "model": { + "label": "Detector-specifieke modelconfiguratie", + "description": "Detector-specifieke modelconfiguratie-opties (pad, invoergrootte, enz.).", + "path": { + "label": "Pad naar aangepast objectdetectormodel", + "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)." + }, + "labelmap_path": { + "label": "Labelmap voor aangepaste objectdetector", + "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector." + }, + "width": { + "label": "Invoerbreedte objectdetectiemodel", + "description": "Breedte van de modelinvoertensor in pixels." + }, + "height": { + "label": "Invoerhoogte objectdetectiemodel", + "description": "Hoogte van de modelinvoertensor in pixels." + }, + "labelmap": { + "label": "Labelmap-aanpassing", + "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap." + }, + "attributes_map": { + "label": "Koppeling van objectlabels aan attribuutlabels", + "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Invoertensorvorm van het model", + "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'." + }, + "input_pixel_format": { + "label": "Invoerpixelkleurformaat van het model", + "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'." + }, + "input_dtype": { + "label": "Invoergegevenstype van het model", + "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')." + }, + "model_type": { + "label": "Modeltype voor objectdetectie", + "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors." + } + }, + "model_path": { + "label": "Detector-specifiek modelpad", + "description": "Bestandspad naar het detector-modelbinaire bestand, indien vereist door de gekozen detector." + }, + "axengine": { + "label": "AXEngine NPU", + "description": "AXERA AX650N/AX8850N NPU-detector die gecompileerde .axmodel-bestanden uitvoert via de AXEngine-runtime." + }, + "cpu": { + "label": "CPU", + "description": "CPU TFLite-detector die TensorFlow Lite-modellen uitvoert op de host-CPU zonder hardwareversnelling. Niet aanbevolen.", + "num_threads": { + "label": "Aantal detectiethreads", + "description": "Het aantal threads voor CPU-gebaseerde inferentie." + } + }, + "deepstack": { + "label": "DeepStack", + "description": "DeepStack/CodeProject.AI-detector die afbeeldingen naar een externe DeepStack HTTP API stuurt voor inferentie. Niet aanbevolen.", + "api_url": { + "label": "DeepStack API URL", + "description": "De URL van de DeepStack API." + }, + "api_timeout": { + "label": "DeepStack API-timeout (in seconden)", + "description": "Maximale toegestane tijd voor een DeepStack API-verzoek." + }, + "api_key": { + "label": "DeepStack API-sleutel (indien vereist)", + "description": "Optionele API-sleutel voor geauthenticeerde DeepStack-diensten." + } + }, + "degirum": { + "label": "DeGirum", + "description": "DeGirum-detector voor het uitvoeren van modellen via DeGirum-cloud of lokale inferentiediensten.", + "location": { + "label": "Locatie van inferentie-engine", + "description": "Locatie van de DeGirum-inferentie-engine (bijv. '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Model Zoo", + "description": "Pad of URL naar de DeGirum model zoo." + }, + "token": { + "label": "DeGirum-cloudtoken", + "description": "Token voor toegang tot de DeGirum-cloud." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "EdgeTPU-detector die TensorFlow Lite-modellen uitvoert die zijn gecompileerd voor Coral EdgeTPU via de EdgeTPU-delegate.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor EdgeTPU-inferentie (bijv. 'usb', 'pci')." + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "Hailo-8/Hailo-8L-detector die HEF-modellen en de HailoRT SDK gebruikt voor inferentie op Hailo-hardware.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor Hailo-inferentie (bijv. 'PCIe', 'M.2')." + } + }, + "memryx": { + "label": "MemryX", + "description": "MemryX MX3-detector die gecompileerde DFP-modellen uitvoert op MemryX-accelerators.", + "device": { + "label": "Apparaatpad", + "description": "Het apparaat voor MemryX-inferentie (bijv. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "ONNX-detector voor het uitvoeren van ONNX-modellen; gebruikt beschikbare versnellingsbackends (CUDA/ROCm/OpenVINO) indien beschikbaar.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor ONNX-inferentie (bijv. 'AUTO', 'CPU', 'GPU')." + } + }, + "openvino": { + "label": "OpenVINO", + "description": "OpenVINO-detector voor AMD- en Intel-CPU's, Intel GPU's en Intel VPU-hardware.", + "device": { + "label": "Apparaattype", + "description": "Het apparaat voor OpenVINO-inferentie (bijv. 'CPU', 'GPU', 'NPU')." + } + }, + "rknn": { + "label": "RKNN", + "description": "RKNN-detector voor Rockchip NPU's; voert gecompileerde RKNN-modellen uit op Rockchip-hardware.", + "num_cores": { + "label": "Aantal te gebruiken NPU-kernen.", + "description": "Het aantal te gebruiken NPU-kernen (0 voor automatisch)." + } + }, + "synaptics": { + "label": "Synaptics", + "description": "Synaptics NPU-detector voor modellen in .synap-formaat via de Synap SDK op Synaptics-hardware." + }, + "teflon_tfl": { + "label": "Teflon", + "description": "Teflon delegate-detector voor TFLite via de Mesa Teflon delegate-bibliotheek voor GPU-versnelling." + }, + "tensorrt": { + "label": "TensorRT", + "description": "TensorRT-detector voor Nvidia Jetson-apparaten via geserialiseerde TensorRT-engines voor versnelde inferentie.", + "device": { + "label": "GPU-apparaatindex", + "description": "De te gebruiken GPU-apparaatindex." + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "ZMQ IPC-detector die inferentie uitbesteedt aan een extern proces via een ZeroMQ IPC-eindpunt.", + "endpoint": { + "label": "ZMQ IPC-eindpunt", + "description": "Het ZMQ-eindpunt waarmee verbinding wordt gemaakt." + }, + "request_timeout_ms": { + "label": "ZMQ-verzoektimeout in milliseconden", + "description": "Timeout voor ZMQ-verzoeken in milliseconden." + }, + "linger_ms": { + "label": "ZMQ-socket linger in milliseconden", + "description": "Socket linger-periode in milliseconden." + } + } + }, + "model": { + "label": "Detectie model", + "description": "Instellingen voor het configureren van een aangepast objectdetectiemodel en de invoervorm.", + "path": { + "label": "Pad naar aangepast objectdetectormodel", + "description": "Pad naar een aangepast detectiemodel (of plus:// voor Frigate+-modellen)." + }, + "labelmap_path": { + "label": "Labelmap voor aangepaste objectdetector", + "description": "Pad naar een labelmap-bestand dat numerieke klassen koppelt aan string-labels voor de detector." + }, + "width": { + "label": "Invoerbreedte objectdetectiemodel", + "description": "Breedte van de modelinvoertensor in pixels." + }, + "height": { + "label": "Invoerhoogte objectdetectiemodel", + "description": "Hoogte van de modelinvoertensor in pixels." + }, + "labelmap": { + "label": "Labelmap-aanpassing", + "description": "Overschrijvingen of herwijzingen om samen te voegen met de standaard labelmap." + }, + "attributes_map": { + "label": "Koppeling van objectlabels aan attribuutlabels", + "description": "Koppeling tussen objectlabels en attribuutlabels voor metadata (bijv. 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Invoertensorvorm van het model", + "description": "Tensorformaat dat het model verwacht: 'nhwc' of 'nchw'." + }, + "input_pixel_format": { + "label": "Invoerpixelkleurformaat van het model", + "description": "Pixelkleurruimte die het model verwacht: 'rgb', 'bgr' of 'yuv'." + }, + "input_dtype": { + "label": "Invoergegevenstype van het model", + "description": "Gegevenstype van de modelinvoertensor (bijv. 'float32')." + }, + "model_type": { + "label": "Modeltype voor objectdetectie", + "description": "Modelarchitectuurtype van de detector (ssd, yolox, yolonas) voor optimalisatie door sommige detectors." + } + }, + "genai": { + "label": "Generatieve AI-configuratie", + "description": "Instellingen voor geïntegreerde generatieve AI-providers voor het genereren van objectbeschrijvingen en beoordelingssamenvattingen.", + "api_key": { + "label": "API-sleutel", + "description": "API-sleutel vereist door sommige providers (kan ook via omgevingsvariabelen worden ingesteld)." + }, + "base_url": { + "label": "Basis-URL", + "description": "Basis-URL voor zelf-gehoste of compatibele providers (bijv. een Ollama-instantie)." + }, + "model": { + "label": "Model", + "description": "Het model van de provider voor het genereren van beschrijvingen of samenvattingen." + }, + "provider": { + "label": "Provider", + "description": "De te gebruiken GenAI-provider (bijv. ollama, gemini, openai)." + }, + "roles": { + "label": "Rollen", + "description": "GenAI-rollen (chat, beschrijvingen, inbeddingen); één provider per rol." + }, + "provider_options": { + "label": "Provideropties", + "description": "Aanvullende provider-specifieke opties voor de GenAI-client." + }, + "runtime_options": { + "label": "Runtime-opties", + "description": "Runtime-opties die bij elke inferentieaanroep aan de provider worden meegegeven." + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg-instellingen inclusief binaire pad, argumenten, hardwareversnellingsopties en uitvoerargumenten per rol.", + "path": { + "label": "FFmpeg-pad", + "description": "Pad naar het te gebruiken FFmpeg-binaire bestand of een versie-alias (\"5.0\" of \"7.0\")." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "Globale argumenten voor FFmpeg-processen." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor FFmpeg. Provider-specifieke presets worden aanbevolen." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten voor FFmpeg-invoerstromen." + }, + "output_args": { + "label": "Uitvoerargumenten", + "description": "Standaard uitvoerargumenten voor verschillende FFmpeg-rollen zoals detectie en opname.", + "detect": { + "label": "Uitvoerargumenten voor detectie", + "description": "Standaard uitvoerargumenten voor streams met detectierol." + }, + "record": { + "label": "Uitvoerargumenten voor opname", + "description": "Standaard uitvoerargumenten voor streams met opnamerol." + } + }, + "retry_interval": { + "label": "FFmpeg-herverbindingstijd", + "description": "Seconden wachten voor een herverbindingspoging na een mislukte camerastream. Standaard is 10." + }, + "apple_compatibility": { + "label": "Apple-compatibiliteit", + "description": "HEVC-tagging inschakelen voor betere Apple-spelercompatibiliteit bij het opnemen van H.265." + }, + "gpu": { + "label": "GPU-index", + "description": "Standaard GPU-index voor hardwareversnelling indien beschikbaar." + }, + "inputs": { + "label": "Camera-invoer", + "description": "Lijst van invoerstream-definities (paden en rollen) voor deze camera.", + "path": { + "label": "Invoerpad", + "description": "URL of pad van de camera-invoerstroom." + }, + "roles": { + "label": "Invoerrollen", + "description": "Rollen voor deze invoerstroom." + }, + "global_args": { + "label": "FFmpeg globale argumenten", + "description": "FFmpeg globale argumenten voor deze invoerstroom." + }, + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten", + "description": "Hardwareversnellingsargumenten voor deze invoerstroom." + }, + "input_args": { + "label": "Invoerargumenten", + "description": "Invoerargumenten specifiek voor deze stream." + } + } + }, + "live": { + "label": "Live weergave", + "description": "Instellingen voor de jsmpeg-livestream-resolutie en -kwaliteit. Dit heeft geen invloed op gerestreamde camera's die go2rtc gebruiken voor live weergave.", + "streams": { + "label": "Live streamnamen", + "description": "Koppeling van geconfigureerde streamnamen aan restream/go2rtc-namen voor live weergave." + }, + "height": { + "label": "Live hoogte", + "description": "Hoogte (pixels) voor weergave van de jsmpeg-livestream in de webinterface; moet ≤ hoogte van de detectiestream zijn." + }, + "quality": { + "label": "Live kwaliteit", + "description": "Coderingskwaliteit voor de jsmpeg-stream (1 hoogste, 31 laagste)." + } + }, + "motion": { + "label": "Bewegingsdetectie", + "description": "Standaard bewegingsdetectie-instellingen die worden toegepast op camera's tenzij per camera overschreven.", + "enabled": { + "label": "Bewegingsdetectie inschakelen", + "description": "Bewegingsdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "threshold": { + "label": "Bewegingsdrempel", + "description": "Pixelverschildrempel voor de bewegingsdetector; hogere waarden verminderen de gevoeligheid (bereik 1-255)." + }, + "lightning_threshold": { + "label": "Bliksemdrempel", + "description": "Drempel om korte lichtflitsen te detecteren en te negeren (lager is gevoeliger, waarden tussen 0,3 en 1,0). Dit voorkomt bewegingsdetectie niet volledig; het zorgt er alleen voor dat de detector stopt met het analyseren van extra frames zodra de drempel wordt overschreden. Op beweging gebaseerde opnames worden tijdens deze gebeurtenissen nog steeds aangemaakt." + }, + "skip_motion_threshold": { + "label": "Drempel voor overgeslagen beweging", + "description": "Als ingesteld op een waarde tussen 0,0 en 1,0, en meer dan dit deel van het beeld verandert in één frame, geeft de detector geen bewegingsvakken terug en kalibreert hij direct opnieuw. Dit bespaart CPU en vermindert vals-positieven bij bliksem, stormen e.d., maar kan echte gebeurtenissen zoals PTZ-tracking missen. De afweging is tussen het weggooien van enkele megabytes opnames versus het bekijken van een paar korte clips. Leeg laten (None) om deze functie uit te schakelen." + }, + "improve_contrast": { + "label": "Contrast verbeteren", + "description": "Contrastverbetering op frames toepassen vóór bewegingsanalyse om detectie te verbeteren." + }, + "contour_area": { + "label": "Contouroppervlakte", + "description": "Minimale contouroppervlakte in pixels voor een bewegingscontour om te worden geteld." + }, + "delta_alpha": { + "label": "Delta-alfa", + "description": "Alpha-mengfactor voor frameverschil bij bewegingsberekening." + }, + "frame_alpha": { + "label": "Frame-alfa", + "description": "Alpha-waarde voor het mengen van frames bij bewegingsvoorverwerking." + }, + "frame_height": { + "label": "Framehoogte", + "description": "Hoogte in pixels waarnaar frames worden geschaald bij het berekenen van beweging." + }, + "mask": { + "label": "Maskercoördinaten", + "description": "Geordende x,y-coördinaten die het bewegingsmaskeerpolygoon definiëren voor het in- of uitsluiten van gebieden." + }, + "mqtt_off_delay": { + "label": "MQTT uit-vertraging", + "description": "Seconden wachten na de laatste beweging vóór publicatie van een MQTT 'off'-status." + }, + "enabled_in_config": { + "label": "Originele bewegingsstatus", + "description": "Geeft aan of bewegingsdetectie was ingeschakeld in de originele statische configuratie." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "objects": { + "label": "Objecten", + "description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.", + "track": { + "label": "Te volgen objecten", + "description": "Lijst met objectlabels om te volgen voor alle camera's; kan per camera worden overschreven." + }, + "filters": { + "label": "Objectfilters", + "description": "Filters op gedetecteerde objecten om vals-positieven te verminderen (oppervlakte, verhouding, betrouwbaarheid).", + "min_area": { + "label": "Minimale objectoppervlakte", + "description": "Minimale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "max_area": { + "label": "Maximale objectoppervlakte", + "description": "Maximale detectiekaderoppervlakte (pixels of percentage) voor dit objecttype. Kan pixels (int) of percentage (float tussen 0,000001 en 0,99) zijn." + }, + "min_ratio": { + "label": "Minimale beeldverhouding", + "description": "Minimale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "max_ratio": { + "label": "Maximale beeldverhouding", + "description": "Maximale breedte/hoogte-verhouding voor het detectiekader om te kwalificeren." + }, + "threshold": { + "label": "Betrouwbaarheidsdrempel", + "description": "Gemiddelde detectiebetrouwbaarheidsdrempel om een object als terecht positief te beschouwen." + }, + "min_score": { + "label": "Minimale betrouwbaarheid", + "description": "Minimale detectiebetrouwbaarheid in één frame om het object te tellen." + }, + "mask": { + "label": "Filtermasker", + "description": "Polygooncoördinaten die aangeven waar dit filter van toepassing is in het frame." + }, + "raw_mask": { + "label": "Onbewerkt masker" + } + }, + "mask": { + "label": "Objectmasker", + "description": "Maskeerpolygoon om objectdetectie in bepaalde gebieden te voorkomen." + }, + "raw_mask": { + "label": "Onbewerkt masker" + }, + "genai": { + "label": "GenAI-objectconfiguratie", + "description": "GenAI-opties voor het beschrijven van gevolgde objecten en het versturen van frames voor generatie.", + "enabled": { + "label": "GenAI inschakelen", + "description": "GenAI-beschrijvingen voor gevolgde objecten standaard inschakelen." + }, + "use_snapshot": { + "label": "Snapshots gebruiken", + "description": "Objectsnapshots gebruiken in plaats van miniaturen voor GenAI-beschrijving." + }, + "prompt": { + "label": "Bijschriftprompt", + "description": "Standaard promptsjabloon voor het genereren van beschrijvingen met GenAI." + }, + "object_prompts": { + "label": "Objectprompts", + "description": "Prompts per object voor het aanpassen van GenAI-uitvoer voor specifieke labels." + }, + "objects": { + "label": "GenAI-objecten", + "description": "Lijst van objectlabels die standaard naar GenAI worden gestuurd." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die objecten moeten betreden om in aanmerking te komen voor GenAI-beschrijving." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar GenAI worden gestuurd opslaan voor foutopsporing." + }, + "send_triggers": { + "label": "GenAI-triggers", + "description": "Bepaalt wanneer frames naar GenAI worden gestuurd (bij einde, na updates, enz.).", + "tracked_object_end": { + "label": "Sturen bij beëindiging", + "description": "Een verzoek naar GenAI sturen wanneer het gevolgde object eindigt." + }, + "after_significant_updates": { + "label": "Vroege GenAI-trigger", + "description": "Een verzoek naar GenAI sturen na een bepaald aantal significante updates voor het gevolgde object." + } + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI was ingeschakeld in de originele statische configuratie." + } + } + }, + "record": { + "label": "Opname", + "description": "Opname- en bewaarinstellingen die worden toegepast op camera's tenzij per camera overschreven.", + "enabled": { + "label": "Opname inschakelen", + "description": "Opname voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "expire_interval": { + "label": "Opruiminterval opnames", + "description": "Minuten tussen opruimrondes die verlopen opnamesegmenten verwijderen." + }, + "continuous": { + "label": "Continue bewaring", + "description": "Aantal dagen om opnames te bewaren ongeacht gevolgde objecten of beweging. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "motion": { + "label": "Bewegingsretentie", + "description": "Aantal dagen om opnames veroorzaakt door beweging te bewaren, ongeacht gevolgde objecten. Stel 0 in om alleen opnames van meldingen en detecties te bewaren.", + "days": { + "label": "Bewaardagen", + "description": "Dagen om opnames te bewaren." + } + }, + "detections": { + "label": "Detectieretentie", + "description": "Opname-retentie-instellingen voor detectiegebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "alerts": { + "label": "Meldingsbewaring", + "description": "Opname-retentie-instellingen voor alertgebeurtenissen inclusief pre/post-captureduur.", + "pre_capture": { + "label": "Seconden vóór opname", + "description": "Aantal seconden vóór de detectiegebeurtenis om op te nemen in de opname." + }, + "post_capture": { + "label": "Seconden na opname", + "description": "Aantal seconden na de detectiegebeurtenis om op te nemen in de opname." + }, + "retain": { + "label": "Gebeurtenisbewaring", + "description": "Bewaarinstellingen voor opnames van detectiegebeurtenissen.", + "days": { + "label": "Bewaardagen", + "description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + } + } + }, + "export": { + "label": "Exportconfiguratie", + "description": "Instellingen voor het exporteren van opnames, zoals timelapse en hardwareversnelling.", + "hwaccel_args": { + "label": "Hardwareversnellingsargumenten voor export", + "description": "Hardwareversnellingsargumenten voor export/transcodering." + }, + "max_concurrent": { + "label": "Maximaal aantal gelijktijdige exports", + "description": "Maximum aantal exporttaken dat tegelijk wordt verwerkt." + } + }, + "preview": { + "label": "Voorbeeldconfiguratie", + "description": "Instellingen voor de kwaliteit van opnamevoorbeelden in de UI.", + "quality": { + "label": "Voorbeeldkwaliteit", + "description": "Kwaliteitsniveau voor voorbeelden (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Originele opnamestatus", + "description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie." + } + }, + "review": { + "label": "Beoordeling", + "description": "Instellingen voor meldingen, detecties en GenAI-beoordelingssamenvattingen in de UI en opslag.", + "alerts": { + "label": "Meldingsconfiguratie", + "description": "Instellingen voor welke gevolgde objecten alerts genereren en hoe alerts worden bewaard.", + "enabled": { + "label": "Alerts inschakelen", + "description": "Genereren van meldingen voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "labels": { + "label": "Meldingslabels", + "description": "Lijst met objectlabels die kwalificeren als meldingen (bijv. auto, persoon)." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als melding te worden beschouwd; leeg laten voor elke zone." + }, + "enabled_in_config": { + "label": "Originele meldingsstatus", + "description": "Geeft aan of meldingen oorspronkelijk waren ingeschakeld in de statische configuratie." + }, + "cutoff_time": { + "label": "Afsluitingstijd meldingen", + "description": "Seconden wachten na het uitblijven van melding veroorzakende activiteit voordat een melding wordt afgesloten." + } + }, + "detections": { + "label": "Detectieconfiguratie", + "description": "Instellingen voor welke gevolgde objecten detecties genereren en hoe detecties worden bewaard.", + "enabled": { + "label": "Detecties inschakelen", + "description": "Detecties voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "labels": { + "label": "Detectielabels", + "description": "Lijst met objectlabels die kwalificeren als detectiegebeurtenissen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden om als detectie te worden beschouwd; leeg laten voor elke zone." + }, + "cutoff_time": { + "label": "Afsluitingstijd detecties", + "description": "Seconden wachten na het uitblijven van detectie veroorzakende activiteit voordat een detectie wordt afgesloten." + }, + "enabled_in_config": { + "label": "Originele detectiestatus", + "description": "Geeft aan of detecties oorspronkelijk waren ingeschakeld in de statische configuratie." + } + }, + "genai": { + "label": "GenAI-configuratie", + "description": "Beheert het gebruik van generatieve AI voor het produceren van beschrijvingen en samenvattingen van beoordelingsitems.", + "enabled": { + "label": "GenAI-beschrijvingen inschakelen", + "description": "Door GenAI gegenereerde beschrijvingen en samenvattingen voor beoordelingsitems in- of uitschakelen." + }, + "alerts": { + "label": "GenAI inschakelen voor meldingen", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij meldingsitems." + }, + "detections": { + "label": "GenAI inschakelen voor detecties", + "description": "GenAI gebruiken voor het genereren van beschrijvingen bij detectiebeoordelingen." + }, + "image_source": { + "label": "Afbeeldingsbron voor beoordeling", + "description": "Bron van afbeeldingen naar GenAI ('preview' of 'recordings'); 'recordings' gebruikt hogere kwaliteit maar meer tokens." + }, + "additional_concerns": { + "label": "Aanvullende aandachtspunten", + "description": "Een lijst met aanvullende aandachtspunten die GenAI moet meenemen bij het beoordelen van activiteit op deze camera." + }, + "debug_save_thumbnails": { + "label": "Snapshots opslaan", + "description": "Snapshots die naar de GenAI-provider worden gestuurd opslaan voor foutopsporing." + }, + "enabled_in_config": { + "label": "Originele GenAI-status", + "description": "Geeft aan of GenAI-beoordeling oorspronkelijk was ingeschakeld in de statische configuratie." + }, + "preferred_language": { + "label": "Voorkeurstaal", + "description": "Voorkeurstaal voor gegenereerde antwoorden van de GenAI-provider." + }, + "activity_context_prompt": { + "label": "Activiteitscontextprompt", + "description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen." + } + } + }, + "snapshots": { + "label": "Snapshots", + "description": "Instellingen voor API-gegenereerde snapshots van gevolgde objecten voor alle camera's; kan per camera worden overschreven.", + "enabled": { + "label": "Snapshots inschakelen", + "description": "Het opslaan van snapshots voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "timestamp": { + "label": "Tijdstempel-overlay", + "description": "Een tijdstempel op API-snapshots weergeven." + }, + "bounding_box": { + "label": "Detectiekader-overlay", + "description": "Detectiekaders voor gevolgde objecten tekenen op API-snapshots." + }, + "crop": { + "label": "Snapshot bijsnijden", + "description": "API-snapshots bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een snapshot wordt opgeslagen." + }, + "height": { + "label": "Snapshothoogte", + "description": "Hoogte (pixels) om API-snapshots naar te schalen; leeg laten om de originele grootte te behouden." + }, + "retain": { + "label": "Snapshot-bewaring", + "description": "Bewaarinstellingen voor snapshots inclusief standaarddagen en per-object overschrijvingen.", + "default": { + "label": "Standaard retentie", + "description": "Standaard aantal dagen om snapshots te bewaren." + }, + "mode": { + "label": "Bewaarmodus", + "description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)." + }, + "objects": { + "label": "Objectbewaring", + "description": "Objectspecifieke overschrijvingen voor het aantal bewaardagen van snapshots." + } + }, + "quality": { + "label": "Snapshotkwaliteit", + "description": "Coderingskwaliteit voor opgeslagen snapshots (0-100)." + } + }, + "timestamp_style": { + "label": "Tijdstempelstijl", + "description": "Stijlopties voor tijdstempels in de feed, toegepast op de debugweergave en snapshots.", + "position": { + "label": "Tijdstempelpositie", + "description": "Positie van de tijdstempel op de afbeelding (tl/tr/bl/br)." + }, + "format": { + "label": "Tijdstempelformaat", + "description": "Datumtijdformaatstring voor tijdstempels (Python datetime-formaatcodes)." + }, + "color": { + "label": "Tijdstempelkleur", + "description": "RGB-kleurwaarden voor de tijdstempeltekst (alle waarden 0-255).", + "red": { + "label": "Rood", + "description": "Roodcomponent (0-255) voor de tijdstempelkleur." + }, + "green": { + "label": "Groen", + "description": "Groencomponent (0-255) voor de tijdstempelkleur." + }, + "blue": { + "label": "Blauw", + "description": "Blauwcomponent (0-255) voor de tijdstempelkleur." + } + }, + "thickness": { + "label": "Tijdstempeldikte", + "description": "Lijndikte van de tijdstempeltekst." + }, + "effect": { + "label": "Tijdstempeleffect", + "description": "Visueel effect voor de tijdstempeltekst (geen, effen, schaduw)." + } + }, + "classification": { + "label": "Objectclassificatie", + "description": "Instellingen voor classificatiemodellen die worden gebruikt om objectlabels of statusclassificatie te verfijnen.", + "bird": { + "label": "Vogelclassificatieconfiguratie", + "description": "Instellingen specifiek voor vogelclassificatiemodellen.", + "enabled": { + "label": "Vogelclassificatie", + "description": "Vogelclassificatie in- of uitschakelen." + }, + "threshold": { + "label": "Minimale score", + "description": "Minimale classificatiescore om een vogelclassificatie te accepteren." + } + }, + "custom": { + "label": "Aangepaste classificatiemodellen", + "description": "Configuratie voor aangepaste classificatiemodellen voor object- of statusdetectie.", + "enabled": { + "label": "Model inschakelen", + "description": "Het aangepaste classificatiemodel in- of uitschakelen." + }, + "name": { + "label": "Modelnaam", + "description": "Identifier van het te gebruiken aangepaste classificatiemodel." + }, + "threshold": { + "label": "Scoredrempel", + "description": "Scoredrempel voor het wijzigen van de classificatiestatus." + }, + "save_attempts": { + "label": "Opgeslagen pogingen", + "description": "Aantal classificatiepogingen dat wordt bijgehouden voor de recente classificaties in de UI." + }, + "object_config": { + "objects": { + "label": "Objecten classificeren", + "description": "Lijst van objecttypen waarop objectclassificatie wordt uitgevoerd." + }, + "classification_type": { + "label": "Classificatietype", + "description": "Toegepast classificatietype: 'sub_label' (voegt sub_label toe) of andere ondersteunde typen." + } + }, + "state_config": { + "cameras": { + "label": "Classificatiecamera's", + "description": "Per-camera bijsnijdinstellingen voor statusclassificatie.", + "crop": { + "label": "Classificatie-uitsnede", + "description": "Bijsnijdcoördinaten voor classificatie op deze camera." + } + }, + "motion": { + "label": "Uitvoeren bij beweging", + "description": "Indien ingeschakeld, classificatie uitvoeren wanneer beweging wordt gedetecteerd in het opgegeven bijsnijdgebied." + }, + "interval": { + "label": "Classificatie-interval", + "description": "Interval (seconden) tussen periodieke classificatierondes voor statusclassificatie." + } + } + } + }, + "semantic_search": { + "label": "Semantisch zoeken", + "description": "Instellingen voor semantisch zoeken, dat objectinbeddingen opbouwt en bevraagt om vergelijkbare items te vinden.", + "enabled": { + "label": "Semantisch zoeken inschakelen", + "description": "De semantisch zoeken-functie in- of uitschakelen." + }, + "reindex": { + "label": "Herindexeren bij opstarten", + "description": "Een volledige herindexering van historische gevolgde objecten in de inbeddingsdatabase starten." + }, + "model": { + "label": "Semantisch zoekmodel of GenAI-providernaam", + "description": "Het inbeddingsmodel voor semantisch zoeken (bijv. 'jinav1'), of de naam van een GenAI-provider met de inbeddingsrol." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Selecteer modelgrootte; 'small' draait op CPU en 'large' vereist doorgaans een GPU." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + }, + "triggers": { + "label": "Triggers", + "description": "Acties en matchcriteria voor cameraspecifieke semantisch-zoeken-triggers.", + "friendly_name": { + "label": "Weergavenaam", + "description": "Optionele weergavenaam voor deze trigger in de UI." + }, + "enabled": { + "label": "Trigger inschakelen", + "description": "Deze semantisch-zoeken-trigger in- of uitschakelen." + }, + "type": { + "label": "Triggertype", + "description": "Type trigger: 'thumbnail' (vergelijk met afbeelding) of 'description' (vergelijk met tekst)." + }, + "data": { + "label": "Triggerinhoud", + "description": "Tekstzin of miniatuur-ID om te vergelijken met gevolgde objecten." + }, + "threshold": { + "label": "Triggerdrempel", + "description": "Minimale gelijkenisscore (0-1) om deze trigger te activeren." + }, + "actions": { + "label": "Triggeracties", + "description": "Lijst van uit te voeren acties bij triggermatch (melding, sub_label, attribuut)." + } + } + }, + "face_recognition": { + "label": "Gezichtsherkenning", + "description": "Instellingen voor gezichtsdetectie en -herkenning voor alle camera's; kan per camera worden overschreven.", + "enabled": { + "label": "Gezichtsherkenning inschakelen", + "description": "Gezichtsherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor gezichtsinbeddingen (small/large); groter vereist mogelijk een GPU." + }, + "unknown_score": { + "label": "Drempel voor onbekende score", + "description": "Afstandsdrempel waaronder een gezicht als mogelijke match wordt beschouwd (hoger = strikter)." + }, + "detection_threshold": { + "label": "Detectiedrempel", + "description": "Minimale detectiebetrouwbaarheid om een gezichtsdetectie als geldig te beschouwen." + }, + "recognition_threshold": { + "label": "Herkenningsdrempel", + "description": "Gezichtsinbeddingsafstandsdrempel om twee gezichten als match te beschouwen." + }, + "min_area": { + "label": "Minimale gezichtsoppervlakte", + "description": "Minimale oppervlakte (pixels) van een gedetecteerd gezichtskader om herkenning te proberen." + }, + "min_faces": { + "label": "Minimum gezichten", + "description": "Minimum aantal gezichtsherkeningen vereist voordat een herkend sub-label aan een persoon wordt toegekend." + }, + "save_attempts": { + "label": "Opgeslagen pogingen", + "description": "Aantal gezichtsherkenningspogingen dat wordt bijgehouden voor de recente herkenningen in de UI." + }, + "blur_confidence_filter": { + "label": "Vaagheidsbetrouwbaarheidsfilter", + "description": "Betrouwbaarheidsscores aanpassen op basis van beeldvaagheid om vals-positieven bij slechte gezichtskwaliteit te verminderen." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + } + }, + "lpr": { + "label": "Kentekenherkenning", + "description": "Instellingen voor kentekenherkenning inclusief detectiedrempels, opmaak en bekende kentekens.", + "enabled": { + "label": "LPR inschakelen", + "description": "Kentekenherkenning voor alle camera's in- of uitschakelen; kan per camera worden overschreven." + }, + "model_size": { + "label": "Modelgrootte", + "description": "Modelgrootte voor tekstdetectie/-herkenning. De meeste gebruikers moeten 'small' gebruiken." + }, + "detection_threshold": { + "label": "Detectiedrempel", + "description": "Detectiebetrouwbaarheidsdrempel om OCR te starten op een vermoedelijk kenteken." + }, + "min_area": { + "label": "Minimale kentekenoppervlakte", + "description": "Minimale kentekenoppervlakte (pixels) om herkenning te proberen." + }, + "recognition_threshold": { + "label": "Herkenningsdrempel", + "description": "Betrouwbaarheidsdrempel voor herkende kentekentekst om als sub-label toe te voegen." + }, + "min_plate_length": { + "label": "Minimale kentekenlengte", + "description": "Minimum aantal tekens dat een herkend kenteken moet bevatten om geldig te zijn." + }, + "format": { + "label": "Regex voor kentekenformaat", + "description": "Optionele regex om herkende kentekens te valideren tegen een verwacht formaat." + }, + "match_distance": { + "label": "Overeenkomstafstand", + "description": "Aantal toegestane tekenfouten bij vergelijking van gedetecteerde kentekens met bekende kentekens." + }, + "known_plates": { + "label": "Bekende kentekens", + "description": "Lijst met kentekens of regex-patronen om specifiek te volgen of meldingen voor te genereren." + }, + "enhancement": { + "label": "Verbeteringsniveau", + "description": "Verbeteringsniveau (0-10) voor kentekenuitsneden vóór OCR; hogere waarden verbeteren niet altijd het resultaat; niveaus boven 5 werken mogelijk alleen voor nachtelijke kentekens en moeten voorzichtig worden gebruikt." + }, + "debug_save_plates": { + "label": "Kentekenplaten opslaan voor foutopsporing", + "description": "Kentekenuitsneden opslaan voor foutopsporing van LPR-prestaties." + }, + "device": { + "label": "Apparaat", + "description": "Dit is een overschrijving om een specifiek apparaat te targeten. Zie https://onnxruntime.ai/docs/execution-providers/ voor meer informatie" + }, + "replace_rules": { + "label": "Vervangingsregels", + "description": "Regex-vervangingsregels voor het normaliseren van gedetecteerde kentekenstrings vóór vergelijking.", + "pattern": { + "label": "Regex-patroon" + }, + "replacement": { + "label": "Vervangende tekst" + } + }, + "expire_time": { + "label": "Vervaltijd in seconden", + "description": "Tijd in seconden waarna een niet-gezien kenteken vervalt uit de tracker (alleen voor dedicated LPR-camera's)." + } + }, + "camera_groups": { + "label": "Cameragroepen", + "description": "Configuratie voor benoemde cameragroepen voor het organiseren van camera's in de UI.", + "cameras": { + "label": "Cameralijst", + "description": "Lijst met cameranamen in deze groep." + }, + "icon": { + "label": "Groepspictogram", + "description": "Pictogram voor de cameragroep in de UI." + }, + "order": { + "label": "Sorteervolgorde", + "description": "Numerieke volgorde voor het sorteren van cameragroepen in de UI; grotere nummers verschijnen later." + } + }, + "active_profile": { + "label": "Actief profiel", + "description": "Naam van het momenteel actieve profiel. Alleen runtime, wordt niet opgeslagen in YAML." + }, + "camera_mqtt": { + "label": "MQTT", + "description": "Instellingen voor het publiceren van MQTT-afbeeldingen.", + "enabled": { + "label": "Afbeelding versturen", + "description": "Het publiceren van afbeeldingssnapshots van objecten naar MQTT-topics voor deze camera inschakelen." + }, + "timestamp": { + "label": "Tijdstempel toevoegen", + "description": "Een tijdstempel op naar MQTT gepubliceerde afbeeldingen weergeven." + }, + "bounding_box": { + "label": "Detectiekader toevoegen", + "description": "Detectiekaders tekenen op via MQTT gepubliceerde afbeeldingen." + }, + "crop": { + "label": "Afbeelding bijsnijden", + "description": "Naar MQTT gepubliceerde afbeeldingen bijsnijden tot het detectiekader van het gedetecteerde object." + }, + "height": { + "label": "Afbeeldingshoogte", + "description": "Hoogte (pixels) voor het schalen van via MQTT gepubliceerde afbeeldingen." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Zones die een object moet betreden voordat een MQTT-afbeelding wordt gepubliceerd." + }, + "quality": { + "label": "JPEG-kwaliteit", + "description": "JPEG-kwaliteit voor naar MQTT gepubliceerde afbeeldingen (0-100)." + } + }, + "camera_ui": { + "label": "Camera-UI", + "description": "Weergavevolgorde en zichtbaarheid van deze camera in de UI. De volgorde heeft invloed op het standaarddashboard. Gebruik cameragroepen voor fijnere controle.", + "order": { + "label": "UI-volgorde", + "description": "Numerieke volgorde voor het sorteren van de camera in de UI (standaarddashboard en lijsten); grotere nummers verschijnen later." + }, + "dashboard": { + "label": "Tonen in UI", + "description": "Schakel de zichtbaarheid van deze camera overal in de Frigate-UI in of uit. Uitschakelen vereist handmatige aanpassing van de configuratie om de camera opnieuw te bekijken." + } + }, + "onvif": { + "label": "ONVIF", + "description": "ONVIF-verbindings- en PTZ-autovolgingsinstellingen voor deze camera.", + "host": { + "label": "ONVIF-host", + "description": "Host (en optioneel schema) voor de ONVIF-dienst van deze camera." + }, + "port": { + "label": "ONVIF-poort", + "description": "Poortnummer voor de ONVIF-dienst." + }, + "user": { + "label": "ONVIF-gebruikersnaam", + "description": "Gebruikersnaam voor ONVIF-authenticatie; sommige apparaten vereisen de admin-gebruiker voor ONVIF." + }, + "password": { + "label": "ONVIF-wachtwoord", + "description": "Wachtwoord voor ONVIF-authenticatie." + }, + "tls_insecure": { + "label": "TLS-verificatie uitschakelen", + "description": "TLS-verificatie overslaan en digest-authenticatie uitschakelen voor ONVIF (onveilig; alleen in veilige netwerken)." + }, + "profile": { + "label": "ONVIF-profiel", + "description": "Specifiek ONVIF-mediaprofiel voor PTZ-besturing, gekoppeld via token of naam. Indien niet ingesteld, wordt het eerste profiel met geldige PTZ-configuratie automatisch geselecteerd." + }, + "autotracking": { + "label": "Automatisch volgen", + "description": "Bewegende objecten automatisch volgen en gecentreerd houden in het beeld via PTZ-camerabewegingen.", + "enabled": { + "label": "Automatisch volgen inschakelen", + "description": "Automatisch PTZ-camera volgen van gedetecteerde objecten in- of uitschakelen." + }, + "calibrate_on_startup": { + "label": "Kalibreren bij opstarten", + "description": "PTZ-motorsnelheden meten bij opstarten voor nauwkeurigere volging. Frigate werkt de configuratie bij met movement_weights na kalibratie." + }, + "zooming": { + "label": "Zoommodus", + "description": "Zoomgedrag instellen: disabled (alleen pan/tilt), absolute (meest compatibel) of relative (gelijktijdig pan/tilt/zoom)." + }, + "zoom_factor": { + "label": "Zoomfactor", + "description": "Zoomniveau voor gevolgde objecten instellen. Lagere waarden tonen meer van de scène; hogere waarden zoomen verder in maar kunnen de volging verliezen. Waarden tussen 0,1 en 0,75." + }, + "track": { + "label": "Gevolgde objecten", + "description": "Lijst van objecttypen die automatisch volgen activeren." + }, + "required_zones": { + "label": "Vereiste zones", + "description": "Objecten moeten een van deze zones betreden voordat automatisch volgen begint." + }, + "return_preset": { + "label": "Terugkeer-voorinstelling", + "description": "ONVIF-voorkeuzeinstelling in de camerafirmware om naar terug te keren na het volgen." + }, + "timeout": { + "label": "Terugkeertimeout", + "description": "Dit aantal seconden wachten na het verliezen van de volging voordat de camera naar de voorkeuze-positie terugkeert." + }, + "movement_weights": { + "label": "Bewegingsgewichten", + "description": "Kalibratiewaarden automatisch gegenereerd door camerakalbratie. Niet handmatig aanpassen." + }, + "enabled_in_config": { + "label": "Originele autovolgstatus", + "description": "Intern veld om bij te houden of automatisch volgen was ingeschakeld in de configuratie." + } + }, + "ignore_time_mismatch": { + "label": "Tijdsverschil negeren", + "description": "Tijdsynchronisatieverschillen tussen camera en Frigate-server negeren voor ONVIF-communicatie." + } } } diff --git a/web/public/locales/nl/config/groups.json b/web/public/locales/nl/config/groups.json index 6ecc7a6123..e69cd65051 100644 --- a/web/public/locales/nl/config/groups.json +++ b/web/public/locales/nl/config/groups.json @@ -49,7 +49,7 @@ }, "timestamp_style": { "global": { - "appearance": "Globaal voorkomen" + "appearance": "Algemeen uiterlijk" }, "cameras": { "appearance": "Voorkomen" diff --git a/web/public/locales/nl/config/validation.json b/web/public/locales/nl/config/validation.json index 6ddb7c764b..3c95b49d3d 100644 --- a/web/public/locales/nl/config/validation.json +++ b/web/public/locales/nl/config/validation.json @@ -1,6 +1,6 @@ { "minimum": "Minimale waarde van {{limit}} vereist", - "maximum": "Mag niet meer dan {{limit}} bedragen.", + "maximum": "Mag niet meer dan {{limit}} bedragen", "exclusiveMinimum": "Waarde moet groter zijn dan {{limit}}", "exclusiveMaximum": "Moet minder zijn dan {{limit}}", "minLength": "Moet minstens {{limit}} karakters zijn", diff --git a/web/public/locales/nl/views/chat.json b/web/public/locales/nl/views/chat.json index 0967ef424b..d4ffad1fb8 100644 --- a/web/public/locales/nl/views/chat.json +++ b/web/public/locales/nl/views/chat.json @@ -1 +1,17 @@ -{} +{ + "documentTitle": "Chat - Frigate", + "placeholder": "Stel een vraag...", + "error": "Er is iets misgegaan. Probeer opnieuw.", + "processing": "Verwerken...", + "toolsUsed": "Gebruikt: {{tools}}", + "hideTools": "Gereedschap verbergen", + "call": "Rinkel", + "title": "Frigate Chat", + "subtitle": "Jouw AI assistent voor camera beheer en inzichten", + "result": "Uitkomst", + "arguments": "Argumenten:", + "response": "Antwoord:", + "attachment_chip_remove": "Verwijder bijlage", + "open_in_explore": "Openen in Verken", + "showTools": "Gereedschap tonen" +} diff --git a/web/public/locales/nl/views/classificationModel.json b/web/public/locales/nl/views/classificationModel.json index 7d655b134a..00e6e83285 100644 --- a/web/public/locales/nl/views/classificationModel.json +++ b/web/public/locales/nl/views/classificationModel.json @@ -12,10 +12,10 @@ }, "toast": { "success": { - "deletedCategory_one": "Verwijderde klasse", - "deletedCategory_other": "Verwijderde klassen", - "deletedImage_one": "Verwijderde afbeelding", - "deletedImage_other": "Verwijderde afbeeldingen", + "deletedCategory_one": "Verwijderd {{count}} klasse", + "deletedCategory_other": "Verwijderde {{count}} klassen", + "deletedImage_one": "Verwijderde {{count}} afbeelding", + "deletedImage_other": "Verwijderde {{count}} afbeeldingen", "categorizedImage": "Succesvol geclassificeerde afbeelding", "trainedModel": "Succesvol getraind model.", "trainingModel": "Modeltraining succesvol gestart.", diff --git a/web/public/locales/nl/views/motionSearch.json b/web/public/locales/nl/views/motionSearch.json index 0967ef424b..b289113983 100644 --- a/web/public/locales/nl/views/motionSearch.json +++ b/web/public/locales/nl/views/motionSearch.json @@ -1 +1,65 @@ -{} +{ + "startSearch": "Zoeken Starten", + "searchStarted": "Zoekopdracht gestart", + "searchCancelled": "Zoekopdracht geannuleerd", + "cancelSearch": "Annuleer", + "searching": "Zoekopdracht bezig.", + "searchComplete": "Zoekopdracht voltooid", + "title": "Beweging Zoeken", + "selectCamera": "Beweging Zoeken is aan het laden", + "noResultsYet": "Start een zoekactie om beweging te vinden in de geselecteerde regio", + "noChangesFound": "Geen pixel wijziging gedetecteerd in de geselecteerde regio", + "changesFound_one": "{{count}} bewegingsverandering gevonden", + "changesFound_other": "{{count}} bewegingsveranderingen gevonden", + "framesProcessed": "{{count}} frames verwerkt", + "jumpToTime": "Spring naar deze tijd", + "results": "Resultaten", + "documentTitle": "Beweging Zoeken - Frigate", + "description": "Teken een polygoon om het interessegebied te definieren en specifeer een tijdspanne voor het zoeken in dit gebied.", + "newSearch": "Nieuwe Zoekopdracht", + "clearResults": "Verwijder Resultaten", + "clearROI": "Verwijder Polygoon", + "polygonControls": { + "points_one": "{{count}} punt", + "points_other": "{{count}} punten", + "undo": "Verwijder het laatste punt", + "reset": "Herstel Polygoon" + }, + "dialog": { + "title": "Beweging Zoeken", + "cameraLabel": "Camera" + }, + "timeRange": { + "start": "Starttijd", + "end": "Eindtijd" + }, + "settings": { + "title": "Zoekinstellingen", + "parallelMode": "Parallelle modus", + "parallelModeDesc": "Scan meerdere video segmenten tegelijk (sneller, maar significant meer CPU gebruik)", + "threshold": "Gevoeligheid drempel", + "thresholdDesc": "Lagere waardes detecteren eerder veranderingen (1-255)", + "minArea": "Minimaal wijzigings gebied", + "minAreaDesc": "Minimale percentage van gebied welke moet wijzigen om als significante wijziging aan te merken", + "frameSkip": "Frame overlaan", + "maxResults": "Maximaal aantal resultaten", + "maxResultsDesc": "Stop na dit aantal overeenkomende tijdstempels" + }, + "errors": { + "polygonTooSmall": "De Polygoon moet minstens 3 punten bevatten", + "unknown": "Onbekende fout", + "noCamera": "Selecteer een camera", + "noROI": "Teken een interesse gebied a.u.b.", + "noTimeRange": "Selecteer een tijdsbereik a.u.b.", + "invalidTimeRange": "Eindtijd moet na de starttijd liggen", + "searchFailed": "Zoeken gefaald: {{message}}" + }, + "changePercentage": "{{percentage}}% gewijzigd", + "metrics": { + "title": "Zoek Meetgegevens", + "segmentsScanned": "Gescande segmenten", + "segmentsProcessed": "Verwerkt", + "segmentsSkippedInactive": "Overgeslagen (geen activiteit)", + "segmentsSkippedHeatmap": "Overgeslagen (geen ROI overlap)" + } +} diff --git a/web/public/locales/nl/views/replay.json b/web/public/locales/nl/views/replay.json index 0967ef424b..143c16ec48 100644 --- a/web/public/locales/nl/views/replay.json +++ b/web/public/locales/nl/views/replay.json @@ -1 +1,59 @@ -{} +{ + "websocket_messages": "Berichten", + "dialog": { + "camera": "Broncamera", + "preset": { + "1m": "Laatste 1 minuut", + "5m": "Laatste 5 minuten", + "timeline": "Vanaf tijdlijn", + "custom": "Aangepast" + }, + "title": "Start Debug Herhaling", + "timeRange": "Tijdsbereik", + "startButton": "Start herhaling", + "selectFromTimeline": "Selecteer", + "starting": "Herhaling starten...", + "startLabel": "Start", + "endLabel": "Einde", + "description": "Maak een tijdelijke herhalingscamera die historische beelden in een lus afspeelt voor het debuggen van objectdetectie- en trackingproblemen. De herhalingscamera gebruikt dezelfde detectieconfiguratie als de broncamera. Kies een tijdsbereik om te beginnen.", + "toast": { + "error": "Kan debugherhaling niet starten: {{error}}", + "alreadyActive": "Er is al een herhalingssessie actief", + "stopError": "Kan debugherhaling niet stoppen: {{error}}", + "goToReplay": "Ga naar herhaling" + } + }, + "title": "Debug Herhaling", + "description": "Herhaal camera-opnames voor foutopsporing. De objectlijst toont een vertraagde samenvatting van gedetecteerde objecten en het tabblad Berichten toont een stream van interne Frigate-berichten uit de herhaalde beelden.", + "page": { + "noSession": "Geen actieve debugherhalingssessie", + "noSessionDesc": "Start een debugherhaling vanuit de Geschiedenis-weergave door op de knop Acties in de werkbalk te klikken en Debug Herhaling te kiezen.", + "goToRecordings": "Ga naar Geschiedenis", + "preparingClip": "Clip voorbereiden…", + "preparingClipDesc": "Frigate voegt opnames samen voor het geselecteerde tijdsbereik. Dit kan bij langere bereiken even duren.", + "startingCamera": "Debugherhaling starten…", + "startError": { + "title": "Kan debugherhaling niet starten", + "back": "Terug naar Geschiedenis" + }, + "sourceCamera": "Broncamera", + "replayCamera": "Herhalingscamera", + "initializingReplay": "Debugherhaling initialiseren...", + "stoppingReplay": "Debugherhaling stoppen...", + "stopReplay": "Stop herhaling", + "confirmStop": { + "title": "Debugherhaling stoppen?", + "description": "Dit stopt de sessie en ruimt alle tijdelijke gegevens op. Weet je het zeker?", + "confirm": "Stop herhaling", + "cancel": "Annuleren" + }, + "activity": "Activiteit", + "objects": "Objectlijst", + "audioDetections": "Audiodetecties", + "noActivity": "Geen activiteit gedetecteerd", + "activeTracking": "Actieve tracking", + "noActiveTracking": "Geen actieve tracking", + "configuration": "Configuratie", + "configurationDesc": "Stem de instellingen voor bewegingsdetectie en objecttracking van de debugherhalingscamera nauwkeurig af. Wijzigingen worden niet opgeslagen in je Frigate-configuratiebestand." + } +} diff --git a/web/public/locales/nl/views/settings.json b/web/public/locales/nl/views/settings.json index 1425acd22f..1deff528c8 100644 --- a/web/public/locales/nl/views/settings.json +++ b/web/public/locales/nl/views/settings.json @@ -3,7 +3,7 @@ "default": "Instellingen - Frigate", "camera": "Camera-instellingen - Frigate", "authentication": "Authenticatie-instellingen - Frigate", - "motionTuner": "Motion Tuner - Frigate", + "motionTuner": "Beweging Tuner - Frigate", "classification": "Classificatie-instellingen - Frigate", "masksAndZones": "Masker- en zone-editor - Frigate", "object": "Foutopsporing Frigate", @@ -12,11 +12,12 @@ "notifications": "Meldingsinstellingen - Frigate", "enrichments": "Verrijkingsinstellingen - Frigate", "cameraManagement": "Camera's beheren - Frigate", - "cameraReview": "Camera Review Instellingen - Frigate", - "globalConfig": "Globale configuratie - Frigate", + "cameraReview": "Camera Beoordeling Instellingen - Frigate", + "globalConfig": "Globaale configuratie - Frigate", "cameraConfig": "Camera-instellingen - Frigate", "maintenance": "Onderhoud - Frigate", - "profiles": "Profielen - Frigate" + "profiles": "Profielen - Frigate", + "detectorsAndModel": "Detectoren en model - Frigate" }, "menu": { "ui": "Gebruikersinterface", @@ -34,7 +35,7 @@ "cameraManagement": "Beheer", "cameraReview": "Beoordeel", "general": "Algemeen", - "globalConfig": "Globale configuratie", + "globalConfig": "Globaale configuratie", "system": "Systeem", "integrations": "Integraties", "profileSettings": "Profielinstellingen", @@ -76,7 +77,7 @@ "systemMqtt": "MQTT", "systemEnvironmentVariables": "Omgevingsvariabelen", "systemTelemetry": "Telemetrie", - "systemBirdseye": "Overzicht", + "systemBirdseye": "Birdseye", "systemFfmpeg": "FFmpeg", "systemDetectorHardware": "Detectie hardware", "cameraFaceRecognition": "Gezichtsherkenning", @@ -88,7 +89,12 @@ "cameraOnvif": "ONVIF", "cameraUi": "Camera UI", "cameraTimestampStyle": "Tijdstempel stijl", - "maintenance": "Onderhoud" + "maintenance": "Onderhoud", + "systemDetectorsAndModel": "Detectoren en model", + "cameraBirdseye": "Birdseye", + "cameraMqtt": "Camera MQTT", + "mediaSync": "Media-synchronisatie", + "regionGrid": "Regio-raster" }, "dialog": { "unsavedChanges": { @@ -352,12 +358,27 @@ "zone": "zone", "motion_mask": "bewegingsmasker", "object_mask": "objectmasker" + }, + "revertOverride": { + "title": "Terugzetten naar basisconfiguratie", + "desc": "Dit verwijdert de profieloverschrijving voor de {{type}} {{name}} en zet deze terug naar de basisconfiguratie." } }, "speed": { "error": { "mustBeGreaterOrEqualTo": "De snelheidsdrempel moet groter dan of gelijk zijn aan 0,1." } + }, + "id": { + "error": { + "mustNotBeEmpty": "ID mag niet leeg zijn.", + "alreadyExists": "Er bestaat al een masker met deze ID voor deze camera." + } + }, + "name": { + "error": { + "mustNotBeEmpty": "Naam mag niet leeg zijn." + } } }, "zones": { @@ -411,6 +432,10 @@ "allObjects": "Alle objecten", "toast": { "success": "Zone ({{zoneName}}) is opgeslagen." + }, + "enabled": { + "title": "Ingeschakeld", + "description": "Of deze zone actief en ingeschakeld is in het configuratiebestand. Als deze is uitgeschakeld, kan deze niet via MQTT worden ingeschakeld. Uitgeschakelde zones worden tijdens runtime genegeerd." } }, "motionMasks": { @@ -439,7 +464,13 @@ "noName": "Bewegingsmasker is opgeslagen." } }, - "add": "Nieuw bewegingsmasker" + "add": "Nieuw bewegingsmasker", + "defaultName": "Beweging Mask {{number}}", + "name": { + "title": "Name", + "description": "Een optionele vriendelijke naam voor dit bewegingsmasker.", + "placeholder": "Voer een naam in..." + } }, "objectMasks": { "label": "Objectmaskers", @@ -464,11 +495,26 @@ "point_other": "{{count}} punten", "clickDrawPolygon": "Klik om een polygoon op de afbeelding te tekenen.", "context": "Objectfiltermaskers worden gebruikt om valse positieven uit te filteren voor een bepaald objecttype op basis van locatie.", - "edit": "Objectmasker bewerken" + "edit": "Objectmasker bewerken", + "name": { + "title": "Name", + "description": "Een optionele vriendelijke naam voor dit objectmasker.", + "placeholder": "Voer een naam in..." + } }, "restart_required": "Herstart vereist (maskers/zones gewijzigd)", "motionMaskLabel": "Bewegingsmasker {{number}}", - "objectMaskLabel": "Objectmasker {{number}} ({{label}})" + "objectMaskLabel": "Objectmasker {{number}}", + "disabledInConfig": "Item is uitgeschakeld in het configuratiebestand", + "addDisabledProfile": "Voeg dit eerst toe aan de basisconfiguratie en overschrijf het daarna in het profiel", + "profileBase": "(basis)", + "profileOverride": "(overschrijving)", + "masks": { + "enabled": { + "title": "Ingeschakeld", + "description": "Of dit masker is ingeschakeld in het configuratiebestand. Als het is uitgeschakeld, kan het niet via MQTT worden ingeschakeld. Uitgeschakelde maskers worden tijdens runtime genegeerd." + } + } }, "motionDetectionTuner": { "title": "Bewegingsdetectie-afsteller", @@ -500,11 +546,11 @@ "objectList": "Objectenlijst", "noObjects": "Geen objecten", "boundingBoxes": { - "title": "Objectkaders", + "title": "Bewegingskaders", "desc": "Toon objectkaders rond gevolgde objecten", "colors": { "label": "Kleuren van objectkaders", - "info": "
  • Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel.
  • Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd.
  • Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend.
  • Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld).
  • " + "info": "
  • Bij het opstarten wordt er een andere kleur toegewezen aan elk objectlabel
  • Een dunne donkerblauwe lijn geeft aan dat het object op dit moment niet wordt gedetecteerd
  • Een dunne grijze lijn geeft aan dat het object als stilstaand wordt herkend
  • Een dikke lijn geeft aan dat het object het doelwit is van automatische tracking (indien ingeschakeld)
  • " } }, "timestamp": { @@ -646,14 +692,14 @@ "desc": "Machtigingen bijwerken voor {{username}}", "title": "Gebruikersrol wijzigen", "roleInfo": { - "intro": "Selecteer een gepaste rol voor deze gebruiker:", + "intro": "Selectereneer een gepaste rol voor deze gebruiker:", "admin": "Beheerder", "adminDesc": "Volledige toegang tot alle functies.", "viewer": "Kijker", "viewerDesc": "Alleen toegang tot Live-dashboards, Beoordelen, Verkennen en Exports.", "customDesc": "Aangepaste rol met specifieke cameratoegang." }, - "select": "Selecteer een rol" + "select": "Selectereneer een rol" }, "passwordSetting": { "setPassword": "Wachtwoord instellen", @@ -681,7 +727,7 @@ "desc": "Webpushmeldingen vereisen een veilige omgeving (https://…). Dit is een beperking van de browser. Open Frigate via een beveiligde verbinding om meldingen te kunnen ontvangen." }, "globalSettings": { - "title": "Globale instellingen", + "title": "Globaale instellingen", "desc": "Meldingen voor specifieke camera's op alle geregistreerde apparaten tijdelijk uitschakelen." }, "email": { @@ -691,7 +737,7 @@ }, "cameras": { "noCameras": "Geen camera's beschikbaar", - "desc": "Selecteer voor welke camera's je meldingen wilt inschakelen.", + "desc": "Selectereneer voor welke camera's je meldingen wilt inschakelen.", "title": "Camera's" }, "deviceSpecific": "Apparaatspecifieke instellingen", @@ -760,6 +806,14 @@ "plusModelType": { "baseModel": "Basismodel", "userModel": "Verfijnd" + }, + "noModelLoaded": "Er is momenteel geen Frigate+-model geladen.", + "selectModel": "Selecteren a model", + "noModelsAvailable": "Geen modellen beschikbaar", + "filter": { + "ariaLabel": "Modellen filteren op type", + "baseModels": "Basismodellen", + "fineTunedModels": "Verfijnde modellen" } }, "toast": { @@ -767,7 +821,15 @@ "error": "Configuratiewijzigingen konden niet worden opgeslagen: {{errorMessage}}" }, "restart_required": "Herstart vereist (Frigate+ model gewijzigd)", - "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen" + "unsavedChanges": "Niet-opgeslagen wijzigingen in Frigate+ instellingen", + "description": "Frigate+ is een abonnementsdienst die toegang biedt tot extra functies en mogelijkheden voor je Frigate-installatie, waaronder het gebruik van aangepaste objectdetectiemodellen die op je eigen gegevens zijn getraind. Je kunt je Frigate+-modelinstellingen hier beheren.", + "cardTitles": { + "api": "API", + "currentModel": "Huidig model", + "otherModels": "Andere modellen", + "configuration": "Configuratie" + }, + "changeInDetectorsAndModel": "Van model wisselen" }, "enrichments": { "semanticSearch": { @@ -888,13 +950,13 @@ }, "type": { "title": "Type", - "placeholder": "Selecteer het type trigger", + "placeholder": "Selectereneer het type trigger", "description": "Activeer wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd", "thumbnail": "Activeer wanneer een vergelijkbare thumbnail van een gevolgd object wordt gedetecteerd" }, "content": { "title": "Inhoud", - "imagePlaceholder": "Selecteer een thumbnail", + "imagePlaceholder": "Selectereneer een thumbnail", "textPlaceholder": "Tekst invoeren", "imageDesc": "Alleen de meest recente 100 thumbnails worden weergegeven. Als je de gewenste thumbnail niet kunt vinden, bekijk dan eerdere objecten in Verkennen en stel daar een trigger in via het menu.", "textDesc": "Voer tekst in om deze actie te activeren wanneer een vergelijkbare beschrijving van een gevolgd object wordt gedetecteerd.", @@ -1013,7 +1075,7 @@ }, "cameras": { "title": "Camera's", - "desc": "Selecteer de camera's waartoe deze rol toegang heeft. Er is minimaal één camera vereist.", + "desc": "Selectereneer de camera's waartoe deze rol toegang heeft. Er is minimaal één camera vereist.", "required": "Er moet minimaal één camera worden geselecteerd." } } @@ -1052,9 +1114,9 @@ "usernamePlaceholder": "Optioneel", "password": "Wachtwoord", "passwordPlaceholder": "Optioneel", - "selectTransport": "Selecteer transportprotocol", + "selectTransport": "Selectereneer transportprotocol", "cameraBrand": "Cameramerk", - "selectBrand": "Selecteer cameramerk voor URL-sjabloon", + "selectBrand": "Selectereneer cameramerk voor URL-sjabloon", "customUrl": "Aangepaste stream-URL", "brandInformation": "Merkinformatie", "brandUrlFormat": "Voor camera's met het RTSP URL-formaat als: {{exampleUrl}}", @@ -1067,7 +1129,7 @@ "noSnapshot": "Er kan geen snapshot worden opgehaald uit de geconfigureerde stream." }, "errors": { - "brandOrCustomUrlRequired": "Selecteer een cameramerk met host/IP of kies 'Overig' voor een aangepaste URL", + "brandOrCustomUrlRequired": "Selectereneer een cameramerk met host/IP of kies 'Overig' voor een aangepaste URL", "nameRequired": "Cameranaam is vereist", "nameLength": "De cameranaam mag maximaal 64 tekens lang zijn", "invalidCharacters": "Cameranaam bevat ongeldige tekens", @@ -1137,7 +1199,7 @@ "retry": "Opnieuw proberen", "testing": { "probingMetadata": "Camera-metadata onderzoeken...", - "fetchingSnapshot": "Camerasnapshot ophalen..." + "fetchingSnapshot": "Camera'snapshot ophalen..." }, "probeFailed": "Het testen van de camera is mislukt: {{error}}", "probingDevice": "Onderzoekapparaat...", @@ -1208,19 +1270,19 @@ }, "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.", - "streamsTitle": "Camerastreams", + "streamsTitle": "Camera'streams", "addStream": "Stream toevoegen", "addAnotherStream": "Voeg een extra stream toe", "streamUrl": "Stream-URL", "streamUrlPlaceholder": "rtsp://gebruikersnaam:wachtwoord@host:poort/pad", - "selectStream": "Selecteer een stream", + "selectStream": "Selectereneer een stream", "searchCandidates": "Zoek kandidaten...", "noStreamFound": "Geen stream gevonden", "url": "URL", "resolution": "Resolutie", - "selectResolution": "Selecteer resolutie", + "selectResolution": "Selectereneer resolutie", "quality": "Kwaliteit", - "selectQuality": "Selecteer kwaliteit", + "selectQuality": "Selectereneer kwaliteit", "roleLabels": { "detect": "Objectdetectie", "record": "Opname", @@ -1291,7 +1353,8 @@ }, "hikvision": { "substreamWarning": "Substream 1 is beperkt tot een lage resolutie. Veel Hikvision-camera’s ondersteunen extra substreams die in de instellingen van de camera ingeschakeld moeten worden. Het wordt aanbevolen deze streams te controleren en te gebruiken indien beschikbaar." - } + }, + "resolutionUnknown": "De resolutie van deze stream kon niet worden uitgelezen. Stel de detectieresolutie handmatig in via Instellingen of in je configuratie." } } }, @@ -1299,11 +1362,22 @@ "title": "Camera’s beheren", "addCamera": "Nieuwe camera toevoegen", "editCamera": "Camera bewerken:", - "selectCamera": "Selecteer een camera", + "selectCamera": "Selectereneer een camera", "backToSettings": "Terug naar camera-instellingen", "streams": { "title": "Camera's in-/uitschakelen", - "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit." + "desc": "Schakel een camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig door Frigate. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "enableLabel": "Ingeschakeld cameras", + "enableDesc": "Schakel een ingeschakelde camera tijdelijk uit totdat Frigate opnieuw wordt gestart. Het uitschakelen van een camera stopt de verwerking van de streams van deze camera volledig. Detectie, opname en foutopsporing zijn dan niet beschikbaar.
    Let op: dit schakelt go2rtc-restreams niet uit.", + "disableLabel": "Uitgeschakeld cameras", + "disableDesc": "Schakel een camera in die momenteel niet zichtbaar is in de UI en is uitgeschakeld in de configuratie. Na het inschakelen is een herstart van Frigate vereist.", + "enableSuccess": "{{cameraName}} ingeschakeld in de configuratie. Herstart Frigate om de wijzigingen toe te passen.", + "friendlyName": { + "edit": "Cameranaam bewerken", + "title": "Weergavenaam bewerken", + "description": "Stel de vriendelijke naam in die voor deze camera in de Frigate-UI wordt weergegeven. Laat leeg om de camera-ID te gebruiken.", + "rename": "Hernoemen" + } }, "cameraConfig": { "add": "Camera toevoegen", @@ -1333,6 +1407,35 @@ "toast": { "success": "Camera {{cameraName}} is succesvol opgeslagen" } + }, + "description": "Voeg camera's toe, bewerk of verwijder ze, bepaal welke camera's zijn ingeschakeld en configureer overschrijvingen per profiel en cameratype. Kies voor streams, detectie, beweging en andere cameraspecifieke instellingen de betreffende sectie onder Cameraconfiguratie.", + "deleteCamera": "Verwijderen Camera", + "deleteCameraDialog": { + "title": "Verwijderen Camera", + "description": "Het verwijderen van een camera verwijdert permanent alle opnames, gevolgde objecten en configuratie voor die camera. Eventuele go2rtc-streams die aan deze camera zijn gekoppeld, moeten mogelijk nog handmatig worden verwijderd.", + "selectPlaceholder": "Kies camera...", + "confirmTitle": "Weet je het zeker?", + "confirmWarning": "Het verwijderen van {{cameraName}} kan niet ongedaan worden gemaakt.", + "deleteExports": "Verwijder ook exports voor deze camera", + "confirmButton": "Verwijderen Permanently", + "success": "Camera {{cameraName}} is succesvol verwijderd", + "error": "Kan camera {{cameraName}} niet verwijderen" + }, + "profiles": { + "title": "Profiel Camera Overrides", + "selectLabel": "Selecteren profile", + "description": "Configureer welke camera's zijn ingeschakeld of uitgeschakeld wanneer een profiel wordt geactiveerd. Camera's die op \"Overnemen\" staan, behouden hun basisstatus.", + "inherit": "Overnemen", + "enabled": "Ingeschakeld", + "disabled": "Uitgeschakeld" + }, + "cameraType": { + "title": "Cameratype", + "label": "Cameratype", + "description": "Stel het type voor elke camera in. Speciale LPR-camera's zijn camera's met één doel en krachtige optische zoom om kentekens van voertuigen op afstand vast te leggen. De meeste camera's moeten het normale cameratype gebruiken, tenzij de camera specifiek voor LPR is bedoeld en een nauwkeurig gericht beeld op kentekens heeft.", + "normal": "Normal", + "dedicatedLpr": "Speciale LPR", + "saveSuccess": "Cameratype voor {{cameraName}} bijgewerkt. Herstart Frigate om de wijzigingen toe te passen." } }, "cameraReview": { @@ -1365,7 +1468,7 @@ }, "unsavedChanges": "Niet-opgeslagen classificatie-instellingen voor {{camera}}", "selectAlertsZones": "Zones selecteren voor meldingen", - "selectDetectionsZones": "Selecteer zones voor detecties", + "selectDetectionsZones": "Selectereneer zones voor detecties", "limitDetections": "Beperk detecties tot specifieke zones", "toast": { "success": "Configuratie voor beoordelingsclassificatie is opgeslagen. Herstart Frigate om de wijzigingen toe te passen." @@ -1376,6 +1479,562 @@ "overriddenGlobal": "Overschreven (globaal)", "overriddenGlobalTooltip": "Deze camera heeft voorrang op de algemene configuratie-instellingen in dit gedeelte", "overriddenBaseConfig": "Overschreven (basis configuratie)", - "overriddenBaseConfigTooltip": "Het profiel {{profile}} heeft voorrang op de configuratie-instellingen in dit gedeelte" + "overriddenBaseConfigTooltip": "Het profiel {{profile}} heeft voorrang op de configuratie-instellingen in dit gedeelte", + "overriddenGlobalHeading_one": "Deze camera overschrijft {{count}} veld uit de globale configuratie:", + "overriddenGlobalHeading_other": "Deze camera overschrijft {{count}} velden uit de globale configuratie:", + "overriddenGlobalNoDeltas": "Deze camera overschrijft de globale configuratie, maar er zijn geen afwijkende veldwaarden.", + "overriddenBaseConfigHeading_one": "Het profiel {{profile}} overschrijft {{count}} veld uit de basisconfiguratie:", + "overriddenBaseConfigHeading_other": "Het profiel {{profile}} overschrijft {{count}} velden uit de basisconfiguratie:", + "overriddenBaseConfigNoDeltas": "Het profiel {{profile}} overschrijft deze sectie, maar er zijn geen afwijkende veldwaarden ten opzichte van de basisconfiguratie.", + "overriddenInCameras": { + "label_one": "Overschreven in {{count}} camera", + "label_other": "Overschreven in {{count}} camera's", + "tooltip_one": "{{count}} camera overschrijft waarden in deze sectie. Klik om details te bekijken.", + "tooltip_other": "{{count}} camera's overschrijven waarden in deze sectie. Klik om details te bekijken.", + "heading_one": "Deze globale sectie bevat velden die in {{count}} camera worden overschreven.", + "heading_other": "Deze globale sectie bevat velden die in {{count}} camera's worden overschreven.", + "othersField_one": "{{count}} andere", + "othersField_other": "{{count}} andere", + "profilePrefix": "{{profile}}-profiel: {{fields}}" + } + }, + "saveAllPreview": { + "title": "Wijzigingen die worden opgeslagen", + "triggerLabel": "Beoordeling pending changes", + "empty": "Geen openstaande wijzigingen.", + "scope": { + "label": "Bereik", + "global": "Globaal", + "camera": "Camera: {{cameraName}}" + }, + "profile": { + "label": "Profiel" + }, + "field": { + "label": "Veld" + }, + "value": { + "label": "Nieuwe waarde", + "reset": "Resetten" + } + }, + "timestampPosition": { + "tl": "Linksboven", + "tr": "Rechtsboven", + "bl": "Linksonder", + "br": "Rechtsonder" + }, + "detectorsAndModel": { + "title": "Detectoren en model", + "description": "Configureer de detector-backend die objectdetectie uitvoert en het model dat daarbij wordt gebruikt. Wijzigingen worden samen opgeslagen zodat de detector en het model gesynchroniseerd blijven.", + "cardTitles": { + "detector": "Detector-hardware", + "model": "Detectie Model" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Aangepast Model" + }, + "mismatch": { + "warning": "Het huidige Frigate+-model \"{{model}}\" vereist de {{required}}-detector. Kies hieronder een compatibel model of schakel over naar Aangepast model voordat je opslaat." + }, + "plusModel": { + "requiresDetector": "Vereist: {{detector}}", + "noModelSelected": "Selecteren a Frigate+ model" + }, + "toast": { + "saveSuccess": "Detector- en modelinstellingen zijn opgeslagen. Herstart Frigate om de wijzigingen toe te passen.", + "saveError": "Kan detector- en modelinstellingen niet opslaan" + }, + "unsavedChanges": "Niet-opgeslagen wijzigingen aan detector en model", + "restartRequired": "Herstart vereist (detector of model gewijzigd)" + }, + "maintenance": { + "title": "Onderhoud", + "sync": { + "title": "Media synchroniseren", + "desc": "Frigate ruimt media periodiek op volgens je retentieconfiguratie. Het is normaal dat er tijdens het gebruik van Frigate enkele verweesde bestanden ontstaan. Gebruik deze functie om verweesde mediabestanden van de schijf te verwijderen die niet langer in de database worden gebruikt.", + "started": "De mediasynchronisatie is gestart.", + "alreadyRunning": "Er wordt al een synchronisatietaak uitgevoerd", + "error": "Kan synchronisatie niet starten", + "currentStatus": "Status", + "jobId": "Verwerkingsnummer", + "startTime": "Starttijd", + "endTime": "Eindtijd", + "statusLabel": "Status", + "results": "Resultaten", + "errorLabel": "Fout", + "mediaTypes": "Mediatypen", + "allMedia": "Alle media", + "dryRun": "Proefdraaien", + "dryRunEnabled": "Er worden geen bestanden verwijderd", + "dryRunDisabled": "Bestanden worden verwijderd", + "force": "Gedwongen", + "forceDesc": "Negeer de veiligheidsdrempel en voltooi de synchronisatie, zelfs als meer dan 50% van de bestanden zou worden verwijderd.", + "verbose": "Uitgebreid", + "verboseDesc": "Schrijf een volledige lijst van verweesde bestanden naar de schijf ter controle.", + "running": "Synchroniseren bezig...", + "start": "Synchronisatie starten", + "inProgress": "Synchronisatie is bezig. Deze pagina is uitgeschakeld.", + "status": { + "queued": "In de wachtrij", + "running": "Bezig", + "completed": "Voltooid", + "failed": "Mislukt", + "notRunning": "Niet actief" + }, + "resultsFields": { + "filesChecked": "Gecontroleerde bestanden", + "orphansFound": "Wezen gevonden", + "orphansDeleted": "Orphans Verwijderend", + "aborted": "Afgebroken. Het verwijderen zou de veiligheidsdrempel overschrijden.", + "error": "Fout", + "totals": "Totalen" + }, + "event_snapshots": "Snapshots van gevolgde objecten", + "event_thumbnails": "Thumbnails van gevolgde objecten", + "review_thumbnails": "Beoordeling Thumbnails", + "previews": "Vooruitblikken", + "exports": "Exports", + "recordings": "Opnames" + }, + "regionGrid": { + "title": "Regio-raster", + "desc": "Het region grid is een optimalisatie die leert waar objecten van verschillende groottes meestal verschijnen in het gezichtsveld van elke camera. Frigate gebruikt deze gegevens om detectieregio's efficiënt te schalen. Het grid wordt na verloop van tijd automatisch opgebouwd uit gegevens van gevolgde objecten.", + "clear": "Raster van de regio wissen", + "clearConfirmTitle": "Raster van de regio wissen", + "clearConfirmDesc": "Het wissen van het region grid wordt niet aanbevolen, tenzij je onlangs de modelgrootte van je detector hebt gewijzigd of de fysieke positie van je camera hebt aangepast en problemen hebt met objecttracking. Het grid wordt na verloop van tijd automatisch opnieuw opgebouwd terwijl objecten worden gevolgd. Een herstart van Frigate is vereist om de wijzigingen toe te passen.", + "clearSuccess": "Het raster van de regio is succesvol gewist", + "clearError": "Kan region grid niet wissen", + "restartRequired": "Herstart vereist om wijzigingen aan het region grid toe te passen" + } + }, + "configForm": { + "global": { + "title": "Globaal Instellingen", + "description": "Deze instellingen gelden voor alle camera's, tenzij ze worden overschreven in de cameraspecifieke instellingen." + }, + "camera": { + "title": "Camera Instellingen", + "description": "Deze instellingen gelden alleen voor deze camera en overschrijven de globale instellingen.", + "noCameras": "Geen camera's beschikbaar" + }, + "advancedSettingsCount": "Advanced Instellingen ({{count}})", + "advancedCount": "Geavanceerd ({{count}})", + "showAdvanced": "Show Advanced Instellingen", + "tabs": { + "sharedDefaults": "Shared Standaards", + "system": "System", + "integrations": "Integraties" + }, + "additionalProperties": { + "keyLabel": "Sleutel", + "valueLabel": "Waarde", + "keyPlaceholder": "Nieuwe sleutel", + "remove": "Verwijderen" + }, + "knownPlates": { + "namePlaceholder": "bijv. de auto van mijn vrouw", + "platePlaceholder": "Kenteken of reguliere expressie" + }, + "timezone": { + "defaultOption": "Tijdzone van browser gebruiken" + }, + "roleMap": { + "empty": "Geen rolkoppelingen", + "roleLabel": "Role", + "groupsLabel": "Groepen", + "addMapping": "Rolkoppeling toevoegen", + "remove": "Verwijderen" + }, + "ffmpegArgs": { + "preset": "Voorinstelling", + "manual": "Handmatige argumenten", + "inherit": "Overnemen van camera-instelling", + "none": "Geen", + "useGlobalSetting": "Overnemen uit algemene instelling", + "selectPreset": "Selecteren preset", + "manualPlaceholder": "Voer FFmpeg-argumenten in", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (Generiek)", + "preset-http-mjpeg-generic": "HTTP MJPEG (Generiek)", + "preset-http-reolink": "HTTP - Reolink Camera's", + "preset-rtmp-generic": "RTMP (Generiek)", + "preset-rtsp-generic": "RTSP (Generiek)", + "preset-rtsp-restream": "RTSP - her-stream van go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - her-stream van go2rtc (Lage latentie)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Opnemen (generiek, geen audio)", + "preset-record-generic-audio-copy": "Opnemen (Generiek + Audio kopiëren)", + "preset-record-generic-audio-aac": "Opnemen (generiek + audio naar AAC)", + "preset-record-mjpeg": "Record - MJPEG Camera's", + "preset-record-jpeg": "Record - JPEG Camera's", + "preset-record-ubiquiti": "Record - Ubiquiti Camera's" + } + }, + "cameraInputs": { + "itemTitle": "Stream {{index}}" + }, + "restartRequiredField": "Herstart vereist", + "restartRequiredFooter": "Configuratie changed - Restart required", + "sections": { + "detect": "Detectie", + "record": "Opname", + "snapshots": "Snapshots", + "motion": "Beweging", + "objects": "Objecten", + "review": "Beoordeling", + "audio": "Audio", + "notifications": "Meldingen", + "live": "Live weergaven", + "timestamp_style": "Tijdstempels", + "mqtt": "MQTT", + "database": "Database", + "telemetry": "Telemetrie", + "auth": "Authenticatie", + "tls": "TLS", + "proxy": "Proxy", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg", + "detectors": "Detectoren", + "model": "Model", + "semantic_search": "Semantic Zoeken", + "genai": "GenAI", + "face_recognition": "Gezichtsherkenning", + "lpr": "Kentekenherkenning", + "birdseye": "Birdseye", + "masksAndZones": "Maskers / Zones" + }, + "detect": { + "title": "Detectie Instellingen" + }, + "detectors": { + "title": "Detector Instellingen", + "singleType": "Er is slechts één {{type}}-detector toegestaan.", + "keyRequired": "Detectornaam is vereist.", + "keyDuplicate": "De naam van de detector bestaat al.", + "noSchema": "Geen detectorschema's beschikbaar.", + "none": "Geen detectorinstanties geconfigureerd.", + "add": "Detector toevoegen", + "addCustomKey": "Aangepaste sleutel toevoegen" + }, + "record": { + "title": "Opname Instellingen" + }, + "snapshots": { + "title": "Snapshot Instellingen" + }, + "motion": { + "title": "Beweging Instellingen" + }, + "objects": { + "title": "Object Instellingen" + }, + "audioLabels": { + "summary": "{{count}} audiolabels geselecteerd", + "empty": "Geen audiolabels beschikbaar" + }, + "objectLabels": { + "summary": "{{count}} objecttypen geselecteerd", + "empty": "Geen objectlabels beschikbaar" + }, + "reviewLabels": { + "summary": "{{count}} labels geselecteerd", + "empty": "Geen labels beschikbaar" + }, + "filters": { + "objectFieldLabel": "{{field}} voor {{label}}" + }, + "zoneNames": { + "summary": "{{count}} geselecteerd", + "empty": "Geen zones beschikbaar" + }, + "inputRoles": { + "summary": "{{count}} rollen geselecteerd", + "empty": "Geen rollen beschikbaar", + "options": { + "detect": "Detecteren", + "record": "Opnemen", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Beschrijvingen", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Selecteren model…", + "builtIn": "Ingebouwde modellen", + "genaiProviders": "Aanbieders van generatieve AI" + }, + "review": { + "title": "Beoordeling Instellingen" + }, + "audio": { + "title": "Audio Instellingen" + }, + "notifications": { + "title": "Melding Instellingen" + }, + "live": { + "title": "Live View Instellingen" + }, + "timestamp_style": { + "title": "Timestamp Instellingen" + }, + "searchPlaceholder": "Zoeken...", + "addCustomLabel": "Aangepast label toevoegen...", + "genaiModel": { + "placeholder": "Selecteren model…", + "search": "Zoeken models…", + "noModels": "Geen modellen beschikbaar" + } + }, + "globalConfig": { + "title": "Globaal Configuratie", + "description": "Configureer globale instellingen die op alle camera's van toepassing zijn, tenzij ze worden overschreven.", + "toast": { + "success": "Globaal settings saved successfully", + "error": "Kan globale instellingen niet opslaan", + "validationError": "Validatie is mislukt" + } + }, + "cameraConfig": { + "title": "Camera Configuratie", + "description": "Configure settings for individual cameras. Instellingen override global defaults.", + "overriddenBadge": "Overschreven", + "resetToGlobal": "Resetten to Globaal", + "toast": { + "success": "Camera-instellingen zijn succesvol opgeslagen", + "error": "Kan camera-instellingen niet opslaan" + } + }, + "toast": { + "success": "Instellingen saved successfully", + "applied": "Instellingen applied successfully", + "successRestartRequired": "Instellingen saved successfully. Restart Frigate to apply your changes.", + "error": "Kan instellingen niet opslaan", + "validationError": "Validatie mislukt: {{message}}", + "resetSuccess": "Resetten to global defaults", + "resetError": "Kan instellingen niet resetten", + "saveAllSuccess_one": "Opslaand {{count}} section successfully.", + "saveAllSuccess_other": "Alle {{count}} secties zijn succesvol opgeslagen.", + "saveAllPartial_one": "{{successCount}} van {{totalCount}} sectie opgeslagen. {{failCount}} mislukt.", + "saveAllPartial_other": "{{successCount}} van {{totalCount}} secties opgeslagen. {{failCount}} mislukt.", + "saveAllFailure": "Kan niet alle secties opslaan." + }, + "profiles": { + "title": "Profielen", + "activeProfile": "Active Profiel", + "noActiveProfile": "Geen actief profiel", + "active": "Active", + "activated": "Profiel '{{profile}}' activated", + "activateFailed": "Kan profiel niet instellen", + "deactivated": "Profiel deactivated", + "noProfiles": "Geen profielen gedefinieerd.", + "noOverrides": "Geen overschrijvingen", + "cameraCount_one": "{{count}} camera", + "cameraCount_other": "{{count}} cameras", + "columnCamera": "Camera", + "columnOverrides": "Profiel Overrides", + "baseConfig": "Basisconfiguratie", + "addProfile": "Toevoegen Profiel", + "newProfile": "New Profiel", + "profileNamePlaceholder": "bijv. Ingeschakeld, Afwezig, Nachtmodus", + "friendlyNameLabel": "Profiel Name", + "profileIdLabel": "Profiel ID", + "profileIdDescription": "Interne identificatie die wordt gebruikt in configuratie en automatiseringen", + "nameInvalid": "Alleen kleine letters, cijfers en onderstrepingstekens zijn toegestaan", + "nameDuplicate": "Er bestaat al een profiel met deze naam", + "error": { + "mustBeAtLeastTwoCharacters": "Moet minimaal 2 tekens bevatten", + "mustNotContainPeriod": "Mag geen punten bevatten", + "alreadyExists": "Er bestaat al een profiel met deze ID" + }, + "renameProfile": "Rename Profiel", + "renameSuccess": "Profiel renamed to '{{profile}}'", + "deleteProfile": "Verwijderen Profiel", + "deleteProfileConfirm": "Profiel \"{{profile}}\" van alle camera's verwijderen? Dit kan niet ongedaan worden gemaakt.", + "deleteSuccess": "Profiel '{{profile}}' deleted", + "createSuccess": "Profiel '{{profile}}' created", + "removeOverride": "Verwijderen Profiel Override", + "deleteSection": "Verwijderen Section Overrides", + "deleteSectionConfirm": "De {{section}}-overschrijvingen voor profiel {{profile}} op {{camera}} verwijderen?", + "deleteSectionSuccess": "{{section}}-overschrijvingen voor {{profile}} verwijderd", + "enableSwitch": "Enable Profielen", + "enabledDescription": "Profielen zijn ingeschakeld. Maak hieronder een nieuw profiel aan, ga naar een cameraconfiguratiesectie om je wijzigingen aan te brengen en sla op om de wijzigingen toe te passen.", + "disabledDescription": "Met profielen kun je benoemde sets van cameraconfiguratie-overschrijvingen definiëren (bijv. ingeschakeld, afwezig, nacht) die op verzoek kunnen worden geactiveerd." + }, + "unsavedChanges": "Er zijn wijzigingen die nog niet zijn opgeslagen", + "confirmReset": "Confirm Resetten", + "resetToDefaultDescription": "Dit zet alle instellingen in deze sectie terug naar hun standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.", + "resetToGlobalDescription": "Dit zet de instellingen in deze sectie terug naar de globale standaardwaarden. Deze actie kan niet ongedaan worden gemaakt.", + "go2rtcStreams": { + "title": "go2rtc Streams", + "description": "Beheer go2rtc-streamconfiguraties voor het restreamen van camera's. Elke stream heeft een naam en één of meer bron-URL's.", + "addStream": "Stream toevoegen", + "addStreamDesc": "Voer een naam in voor de nieuwe stream. Deze naam wordt gebruikt om naar de stream te verwijzen in je cameraconfiguratie.", + "addUrl": "URL toevoegen", + "streamName": "Stream naam", + "streamNamePlaceholder": "bijv. voor_deur", + "streamUrlPlaceholder": "bijv, rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "Verwijderen stream", + "deleteStreamConfirm": "Weet je zeker dat je de stream \"{{streamName}}\" wilt verwijderen? Camera's die naar deze stream verwijzen, werken mogelijk niet meer.", + "noStreams": "Geen go2rtc-streams geconfigureerd. Voeg een stream toe om te beginnen.", + "validation": { + "nameRequired": "Streamnaam is vereist", + "nameDuplicate": "Er bestaat al een stream met deze naam", + "nameInvalid": "Streamnaam mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten", + "urlRequired": "Er is minimaal één URL vereist" + }, + "renameStream": "Stream hernoemen", + "renameStreamDesc": "Voer een nieuwe naam in voor deze stream. Het hernoemen van een stream kan camera's of andere streams die er op naam naar verwijzen verstoren.", + "newStreamName": "Nieuwe stream naam", + "ffmpeg": { + "useFfmpegModule": "Compatibiliteitsmodus gebruiken (ffmpeg)", + "video": "Video", + "audio": "Audio", + "hardware": "Hardware-versnelling", + "videoCopy": "Kopiëren", + "videoH264": "Transcoderen naar H.264", + "videoH265": "Transcoderen naar H.265", + "videoExclude": "Uitsluiten", + "audioCopy": "Kopiëren", + "audioAac": "Transcoderen naar AAC", + "audioOpus": "Transcoderen naar Opus", + "audioPcmu": "Transcoderen naar PCM μ-law", + "audioPcma": "Transcoderen naar PCM A-law", + "audioPcm": "Transcoderen naar PCM", + "audioMp3": "Transcoderen naar MP3", + "audioExclude": "Uitsluiten", + "hardwareNone": "Geen hardwareversnelling", + "hardwareAuto": "Automatische hardware-versnelling" + } + }, + "birdseye": { + "trackingMode": { + "objects": "Objecten", + "motion": "Beweging", + "continuous": "Doorlopend" + } + }, + "retainMode": { + "all": "Alle", + "motion": "Beweging", + "active_objects": "Active Objecten" + }, + "previewQuality": { + "very_high": "Zeer hoog", + "high": "High", + "medium": "Medium", + "low": "Low", + "very_low": "Zeer laag" + }, + "ui": { + "timeFormat": { + "browser": "Browser", + "12hour": "12 uur", + "24hour": "24 uur" + }, + "TimeOrDateStyle": { + "full": "Full", + "long": "Lang", + "medium": "Medium", + "short": "Kort" + }, + "unitSystem": { + "metric": "Metrisch", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Opnames", + "previews": "Voorbeelden" + } + }, + "logger": { + "logLevel": { + "debug": "Foutopsporing", + "info": "Info", + "warning": "Waarschuwing", + "error": "Fout", + "critical": "Kritisch" + } + }, + "onvif": { + "profileAuto": "Auto", + "profileLoading": "Profielen laden...", + "autotracking": { + "zooming": { + "disabled": "Uitgeschakeld", + "absolute": "Absoluut", + "relative": "Relatief" + } + } + }, + "modelSize": { + "small": "Klein", + "large": "Large" + }, + "configMessages": { + "review": { + "recordDisabled": "Opname is disabled, review items will not be generated.", + "detectDisabled": "Object detection is disabled. Beoordeling items require detected objects to categorize alerts and detections.", + "allNonAlertDetections": "Alle activiteit die geen melding is, wordt opgenomen als detecties.", + "genaiImageSourceRecordingsRecordDisabled": "De afbeeldingsbron is ingesteld op 'recordings', maar opnemen is uitgeschakeld. Frigate valt terug op voorbeeldafbeeldingen." + }, + "audio": { + "noAudioRole": "Er zijn geen streams met de audiorol gedefinieerd. Je moet de audiorol inschakelen om audiodetectie te laten werken." + }, + "audioTranscription": { + "audioDetectionDisabled": "Audiodetectie is niet ingeschakeld voor deze camera. Audiotranscriptie vereist dat audiodetectie actief is." + }, + "detect": { + "fpsGreaterThanFive": "Het instellen van de detectie-FPS hoger dan 5 wordt niet aanbevolen. Hogere waarden kunnen prestatieproblemen veroorzaken en leveren geen voordeel op.", + "disabled": "Objectdetectie is uitgeschakeld. Snapshots, beoordelingsitems en verrijkingen zoals gezichtsherkenning, kentekenherkenning en generatieve AI werken dan niet." + }, + "objects": { + "genaiNoDescriptionsProvider": "Je moet een GenAI-provider configureren met de rol 'descriptions' om beschrijvingen te kunnen genereren." + }, + "faceRecognition": { + "globalDisabled": "De verrijking voor gezichtsherkenning moet zijn ingeschakeld om gezichtsherkenningsfuncties op deze camera te laten werken.", + "personNotTracked": "Gezichtsherkenning vereist dat het object 'person' wordt gevolgd. Schakel 'person' in bij Objecten voor deze camera.", + "modelSizeLarge": "Het 'large'-model vereist een GPU of NPU voor redelijke prestaties. Gebruik 'small' op systemen met alleen een CPU." + }, + "lpr": { + "globalDisabled": "De verrijking voor kentekenherkenning moet zijn ingeschakeld om LPR-functies op deze camera te laten werken.", + "vehicleNotTracked": "Kentekenherkenning vereist dat 'car' of 'motorcycle' wordt gevolgd. Schakel 'car' of 'motorcycle' in bij Objecten voor deze camera.", + "modelSizeLarge": "Het 'large'-model is geoptimaliseerd voor kentekenplaten met meerdere regels. Het 'small'-model presteert beter dan 'large' en moet worden gebruikt tenzij jouw regio kentekenformaten met meerdere regels gebruikt." + }, + "record": { + "noRecordRole": "Er zijn geen streams met de opnamerol gedefinieerd. Opnemen werkt dan niet." + }, + "birdseye": { + "objectsModeDetectDisabled": "Birdseye staat ingesteld op de modus 'objects', maar objectdetectie is uitgeschakeld voor deze camera. De camera wordt niet weergegeven in Birdseye." + }, + "snapshots": { + "detectDisabled": "Objectdetectie is uitgeschakeld. Snapshots worden gegenereerd uit gevolgde objecten en worden daarom niet aangemaakt." + }, + "detectors": { + "mixedTypes": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren om een ander type te gebruiken.", + "mixedTypesSuggestion": "Alle detectoren moeten hetzelfde type gebruiken. Verwijder bestaande detectoren of selecteer {{type}}." + }, + "semanticSearch": { + "jinav2SmallModelSize": "De 'small'-grootte met het Jina V2-model heeft hoge RAM- en inferentiekosten. Het 'large'-model met een aparte GPU wordt aanbevolen." + } } } From 2ae415be6bff5a7d690f77e650b1905744e170d1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:54 +0200 Subject: [PATCH 21/94] Translated using Weblate (Nepali) Currently translated at 29.7% (14 of 47 strings) Translated using Weblate (Nepali) Currently translated at 53.8% (14 of 26 strings) Translated using Weblate (Nepali) Currently translated at 16.2% (14 of 86 strings) Translated using Weblate (Nepali) Currently translated at 14.0% (14 of 100 strings) Translated using Weblate (Nepali) Currently translated at 100.0% (10 of 10 strings) Translated using Weblate (Nepali) Currently translated at 23.7% (14 of 59 strings) Translated using Weblate (Nepali) Currently translated at 56.0% (14 of 25 strings) Translated using Weblate (Nepali) Currently translated at 1.1% (13 of 1122 strings) Translated using Weblate (Nepali) Currently translated at 32.5% (13 of 40 strings) Translated using Weblate (Nepali) Currently translated at 63.6% (14 of 22 strings) Translated using Weblate (Nepali) Currently translated at 11.6% (15 of 129 strings) Translated using Weblate (Nepali) Currently translated at 18.9% (14 of 74 strings) Translated using Weblate (Nepali) Currently translated at 24.1% (14 of 58 strings) Translated using Weblate (Nepali) Currently translated at 5.9% (14 of 237 strings) Translated using Weblate (Nepali) Currently translated at 3.1% (15 of 471 strings) Translated using Weblate (Nepali) Currently translated at 28.5% (14 of 49 strings) Translated using Weblate (Nepali) Currently translated at 11.0% (14 of 127 strings) Translated using Weblate (Nepali) Currently translated at 2.2% (18 of 792 strings) Translated using Weblate (Nepali) Currently translated at 9.6% (14 of 145 strings) Translated using Weblate (Nepali) Currently translated at 3.7% (19 of 501 strings) Translated using Weblate (Nepali) Currently translated at 20.3% (13 of 64 strings) Translated using Weblate (Nepali) Currently translated at 13.8% (14 of 101 strings) Translated using Weblate (Nepali) Currently translated at 100.0% (10 of 10 strings) Translated using Weblate (Nepali) Currently translated at 31.1% (14 of 45 strings) Translated using Weblate (Nepali) Currently translated at 8.0% (14 of 175 strings) Translated using Weblate (Nepali) Currently translated at 100.0% (6 of 6 strings) Co-authored-by: Hosted Weblate Co-authored-by: bijaydewan Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ne/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ne/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation Translation: Frigate NVR/audio Translation: Frigate NVR/common Translation: Frigate NVR/components-auth Translation: Frigate NVR/components-camera Translation: Frigate NVR/components-dialog Translation: Frigate NVR/components-filter Translation: Frigate NVR/components-player Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-classificationmodel Translation: Frigate NVR/views-configeditor Translation: Frigate NVR/views-events Translation: Frigate NVR/views-explore Translation: Frigate NVR/views-exports Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-recording Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-search Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/ne/audio.json | 13 +++++- web/public/locales/ne/common.json | 12 +++++- web/public/locales/ne/components/auth.json | 7 ++- web/public/locales/ne/components/camera.json | 17 +++++++- web/public/locales/ne/components/dialog.json | 25 ++++++++++- web/public/locales/ne/components/filter.json | 19 ++++++++ web/public/locales/ne/components/player.json | 17 ++++++++ web/public/locales/ne/config/cameras.json | 22 ++++++++++ web/public/locales/ne/config/global.json | 43 ++++++++++++++++++- web/public/locales/ne/config/groups.json | 27 ++++++++++++ web/public/locales/ne/config/validation.json | 11 ++++- web/public/locales/ne/objects.json | 11 ++++- web/public/locales/ne/views/chat.json | 10 ++++- .../locales/ne/views/classificationModel.json | 12 +++++- web/public/locales/ne/views/configEditor.json | 13 +++++- web/public/locales/ne/views/events.json | 16 ++++++- web/public/locales/ne/views/explore.json | 23 ++++++++++ web/public/locales/ne/views/exports.json | 16 +++++++ web/public/locales/ne/views/faceLibrary.json | 16 ++++++- web/public/locales/ne/views/live.json | 23 ++++++++++ web/public/locales/ne/views/motionSearch.json | 12 +++++- web/public/locales/ne/views/recording.json | 3 +- web/public/locales/ne/views/replay.json | 12 +++++- web/public/locales/ne/views/search.json | 15 ++++++- web/public/locales/ne/views/settings.json | 10 ++++- web/public/locales/ne/views/system.json | 14 +++++- 26 files changed, 400 insertions(+), 19 deletions(-) diff --git a/web/public/locales/ne/audio.json b/web/public/locales/ne/audio.json index 474844fc05..795ce510df 100644 --- a/web/public/locales/ne/audio.json +++ b/web/public/locales/ne/audio.json @@ -6,5 +6,16 @@ "bellow": "तलतिर", "motorcycle": "मोटरसाइकल", "whoop": "हुप (Whoop)", - "whispering": "सानो बोल्दै" + "whispering": "सानो बोल्दै", + "babbling": "बडबडाउँदै", + "bus": "बस", + "laughter": "हाँसो", + "train": "रेल", + "snicker": "स्निकर", + "boat": "डुङ्गा", + "crying": "रुँदै", + "singing": "गाउँदै", + "choir": "गायन यन्त्र", + "yodeling": "योडेलिङ", + "chant": "मन्त्र" } diff --git a/web/public/locales/ne/common.json b/web/public/locales/ne/common.json index f252005ef8..ec2203d22c 100644 --- a/web/public/locales/ne/common.json +++ b/web/public/locales/ne/common.json @@ -3,6 +3,16 @@ "untilForRestart": "फ्रिगेट पुनः सुरु नभएसम्म।", "untilRestart": "पुन: सुरु नभएसम्म", "never": "कहिल्यै होइन", - "ago": "{{timeAgo}} अघि" + "ago": "{{timeAgo}} अघि", + "untilForTime": "{{time}} सम्म", + "justNow": "भर्खरै", + "today": "आज", + "yesterday": "हिजो", + "last7": "पछिल्लो ७ दिन", + "last14": "पछिल्लो १४ दिन", + "last30": "पछिल्लो ३० दिन", + "thisWeek": "यो हप्ता", + "lastWeek": "गत हप्ता", + "thisMonth": "यो महिना" } } diff --git a/web/public/locales/ne/components/auth.json b/web/public/locales/ne/components/auth.json index 81ffe7c340..e61a7b778c 100644 --- a/web/public/locales/ne/components/auth.json +++ b/web/public/locales/ne/components/auth.json @@ -5,7 +5,12 @@ "login": "लगइन", "firstTimeLogin": "पहिलो पटक लग इन गर्ने प्रयास गर्दै हुनुहुन्छ? प्रमाणपत्रहरू फ्रिगेट लगहरूमा छापिएका हुन्छन्।", "errors": { - "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ" + "usernameRequired": "प्रयोगकर्ता नाम आवश्यक छ", + "passwordRequired": "पासवर्ड आवश्यक छ", + "rateLimit": "दर सीमा नाघ्यो। पछि फेरि प्रयास गर्नुहोस्।", + "loginFailed": "लगइन असफल भयो", + "unknownError": "अज्ञात त्रुटि। लगहरू जाँच गर्नुहोस्", + "webUnknownError": "अज्ञात त्रुटि। कन्सोल लगहरू जाँच गर्नुहोस्।" } } } diff --git a/web/public/locales/ne/components/camera.json b/web/public/locales/ne/components/camera.json index 98f5d8338d..59a1682243 100644 --- a/web/public/locales/ne/components/camera.json +++ b/web/public/locales/ne/components/camera.json @@ -6,8 +6,23 @@ "delete": { "label": "क्यामेरा समूह मेटाउनुहोस्", "confirm": { - "title": "मेटाउने पुष्टि गर्नुहोस्" + "title": "मेटाउने पुष्टि गर्नुहोस्", + "desc": "के तपाईं क्यामेरा समूह {{name}} मेटाउन निश्चित हुनुहुन्छ?" } + }, + "name": { + "label": "नाम", + "placeholder": "नाम प्रविष्ट गर्नुहोस्…", + "errorMessage": { + "mustLeastCharacters": "क्यामेरा समूहको नाम कम्तिमा २ वर्णको हुनुपर्छ।", + "exists": "क्यामेरा समूहको नाम पहिले नै अवस्थित छ।", + "nameMustNotPeriod": "क्यामेरा समूहको नाममा पूर्णविराम हुनुहुँदैन।", + "invalid": "क्यामेरा समूहको नाम अमान्य छ।" + } + }, + "cameras": { + "label": "क्यामेराहरू", + "desc": "यस समूहको लागि क्यामेराहरू चयन गर्नुहोस्।" } } } diff --git a/web/public/locales/ne/components/dialog.json b/web/public/locales/ne/components/dialog.json index 634f89b00e..25cdda520d 100644 --- a/web/public/locales/ne/components/dialog.json +++ b/web/public/locales/ne/components/dialog.json @@ -5,7 +5,30 @@ "button": "पुनः सुरु", "restarting": { "title": "फ्रिगेट पुन: सुरु हुँदैछ", - "content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।" + "content": "यो पृष्ठ {{countdown}} सेकेन्डमा पुन: लोड हुनेछ।", + "button": "अहिले नै जबरजस्ती पुन: लोड गर्नुहोस्" + } + }, + "explore": { + "plus": { + "submitToPlus": { + "label": "फ्रिगेट+ मा पेश गर्नुहोस्", + "desc": "तपाईंले बेवास्ता गर्न चाहनुभएको स्थानहरूमा रहेका वस्तुहरू गलत सकारात्मक होइनन्। तिनीहरूलाई गलत सकारात्मकको रूपमा पेश गर्नाले मोडेल भ्रमित हुनेछ।" + }, + "review": { + "question": { + "label": "फ्रिगेट प्लसको लागि यो लेबल पुष्टि गर्नुहोस्", + "ask_a": "के यो वस्तु {{label}} हो?", + "ask_an": "के यो वस्तु {{label}} हो?", + "ask_full": "के यो वस्तु {{untranslatedLabel}} ({{translatedLabel}}) हो?" + }, + "state": { + "submitted": "पेश गरियो" + } + } + }, + "video": { + "viewInHistory": "इतिहासमा हेर्नुहोस्" } } } diff --git a/web/public/locales/ne/components/filter.json b/web/public/locales/ne/components/filter.json index d520a85389..40a28af632 100644 --- a/web/public/locales/ne/components/filter.json +++ b/web/public/locales/ne/components/filter.json @@ -7,5 +7,24 @@ }, "count_one": "{{count}} कक्षा", "count_other": "{{count}} कक्षाहरू" + }, + "labels": { + "label": "लेबलहरू", + "all": { + "title": "सबै लेबलहरू", + "short": "लेबलहरू" + }, + "count_one": "{{count}} लेबल", + "count_other": "{{count}} लेबलहरू" + }, + "zones": { + "label": "क्षेत्रहरू", + "all": { + "title": "सबै क्षेत्रहरू", + "short": "क्षेत्रहरू" + } + }, + "dates": { + "selectPreset": "प्रिसेट चयन गर्नुहोस्…" } } diff --git a/web/public/locales/ne/components/player.json b/web/public/locales/ne/components/player.json index 64663e4518..ffb97d5402 100644 --- a/web/public/locales/ne/components/player.json +++ b/web/public/locales/ne/components/player.json @@ -5,5 +5,22 @@ "title": "यो फ्रेम Frigate+ मा बुझाउने हो?", "submit": "पेश गर्नुहोस्", "previewError": "स्न्यापसट पूर्वावलोकन लोड गर्न सकिएन। रेकर्डिङ यस समयमा उपलब्ध नहुन सक्छ।" + }, + "noRecordingsFoundForThisTime": "यस समयको लागि कुनै रेकर्डिङ फेला परेन", + "livePlayerRequiredIOSVersion": "यस लाइभ स्ट्रिम प्रकारको लागि iOS १७.१ वा सोभन्दा माथिको संस्करण आवश्यक छ।", + "streamOffline": { + "title": "अफलाइन स्ट्रिम गर्नुहोस्", + "desc": "{{cameraName}} detect स्ट्रिममा कुनै पनि फ्रेमहरू प्राप्त भएका छैनन्, त्रुटि लगहरू जाँच गर्नुहोस्" + }, + "cameraDisabled": "क्यामेरा असक्षम पारिएको छ", + "stats": { + "streamType": { + "title": "स्ट्रिम प्रकार:", + "short": "प्रकार" + }, + "bandwidth": { + "title": "ब्यान्डविथ:", + "short": "ब्यान्डविथ" + } } } diff --git a/web/public/locales/ne/config/cameras.json b/web/public/locales/ne/config/cameras.json index 4f9f3489ad..3179ad38dd 100644 --- a/web/public/locales/ne/config/cameras.json +++ b/web/public/locales/ne/config/cameras.json @@ -7,5 +7,27 @@ "friendly_name": { "label": "मैत्रीपूर्ण नाम", "description": "फ्रिगेट UI मा प्रयोग गरिएको क्यामेरा मैत्री नाम" + }, + "enabled": { + "label": "सक्षम पारिएको", + "description": "सक्षम पारिएको" + }, + "audio": { + "label": "अडियो पत्ता लगाउने सुविधा", + "description": "यस क्यामेराको लागि अडियो-आधारित घटना पत्ता लगाउने सेटिङहरू।", + "enabled": { + "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्", + "description": "यस क्यामेराको लागि अडियो घटना पत्ता लगाउने सुविधा सक्षम वा असक्षम पार्नुहोस्।" + }, + "max_not_heard": { + "label": "समयसीमा समाप्त गर्नुहोस्", + "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।" + }, + "min_volume": { + "label": "न्यूनतम भोल्युम" + } + }, + "zones": { + "label": "क्षेत्रहरू" } } diff --git a/web/public/locales/ne/config/global.json b/web/public/locales/ne/config/global.json index 0967ef424b..fe3f978e70 100644 --- a/web/public/locales/ne/config/global.json +++ b/web/public/locales/ne/config/global.json @@ -1 +1,42 @@ -{} +{ + "version": { + "label": "हालको कन्फिगरेसन संस्करण", + "description": "माइग्रेसन वा ढाँचा परिवर्तनहरू पत्ता लगाउन मद्दत गर्न सक्रिय कन्फिगरेसनको संख्यात्मक वा स्ट्रिङ संस्करण।" + }, + "safe_mode": { + "label": "सुरक्षित मोड", + "description": "सक्षम हुँदा, समस्या निवारणको लागि कम सुविधाहरूको साथ सुरक्षित मोडमा फ्रिगेट सुरु गर्नुहोस्।" + }, + "environment_vars": { + "label": "वातावरणीय चरहरू", + "description": "होम असिस्टेन्ट ओएसमा फ्रिगेट प्रक्रियाको लागि सेट गर्नुपर्ने वातावरण चरहरूको कुञ्जी/मान जोडीहरू। गैर-HAOS प्रयोगकर्ताहरूले यसको सट्टा डकर वातावरण चर कन्फिगरेसन प्रयोग गर्नुपर्छ।" + }, + "logger": { + "label": "लगिङ", + "description": "पूर्वनिर्धारित लग शब्दावली र प्रति-घटक लग स्तर ओभरराइडहरू नियन्त्रण गर्दछ।", + "default": { + "label": "लगिङ स्तर", + "description": "पूर्वनिर्धारित विश्वव्यापी लग शब्दावली (डिबग, जानकारी, चेतावनी, त्रुटि)।" + }, + "logs": { + "label": "प्रति-प्रक्रिया लग स्तर", + "description": "विशिष्ट मोड्युलहरूको लागि शब्दावली बढाउन वा घटाउन प्रति-घटक लग स्तर ओभरराइड हुन्छ।" + } + }, + "audio": { + "label": "अडियो पत्ता लगाउने सुविधा", + "enabled": { + "label": "अडियो पत्ता लगाउने सुविधा सक्षम पार्नुहोस्" + }, + "max_not_heard": { + "label": "समयसीमा समाप्त गर्नुहोस्", + "description": "अडियो घटना समाप्त हुनुभन्दा पहिले कन्फिगर गरिएको अडियो प्रकार बिना सेकेन्डको मात्रा।" + }, + "min_volume": { + "label": "न्यूनतम भोल्युम" + } + }, + "auth": { + "label": "प्रमाणीकरण" + } +} diff --git a/web/public/locales/ne/config/groups.json b/web/public/locales/ne/config/groups.json index e80784d181..f3ea1a4af6 100644 --- a/web/public/locales/ne/config/groups.json +++ b/web/public/locales/ne/config/groups.json @@ -12,6 +12,33 @@ "timestamp_style": { "global": { "appearance": "विश्वव्यापी उपस्थिति" + }, + "cameras": { + "appearance": "उपस्थिति" + } + }, + "motion": { + "global": { + "sensitivity": "विश्वव्यापी संवेदनशीलता", + "algorithm": "विश्वव्यापी एल्गोरिथम" + }, + "cameras": { + "sensitivity": "संवेदनशीलता", + "algorithm": "एल्गोरिथ्म" + } + }, + "snapshots": { + "global": { + "display": "विश्वव्यापी प्रदर्शन" + }, + "cameras": { + "display": "प्रदर्शन" + } + }, + "detect": { + "global": { + "resolution": "विश्वव्यापी रिजोल्युसन", + "tracking": "विश्वव्यापी ट्र्याकिङ" } } } diff --git a/web/public/locales/ne/config/validation.json b/web/public/locales/ne/config/validation.json index 7592167cde..ead6ccf550 100644 --- a/web/public/locales/ne/config/validation.json +++ b/web/public/locales/ne/config/validation.json @@ -3,5 +3,14 @@ "maximum": "बढीमा हुनुपर्छ {{limit}}", "exclusiveMinimum": "{{limit}} भन्दा बढी हुनुपर्छ", "exclusiveMaximum": ".{{limit}} भन्दा कम हुनुपर्छ", - "minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।" + "minLength": "कम्तिमा {{limit}} वर्ण(हरू) हुनुपर्छ।", + "maxLength": "बढीमा {{limit}} वर्ण(हरू) हुनु पर्छ", + "minItems": "कम्तिमा {{limit}} वस्तुहरू हुनुपर्छ", + "maxItems": "बढीमा {{limit}} वस्तुहरू हुनुपर्छ", + "pattern": "अमान्य ढाँचा", + "required": "यो क्षेत्र आवश्यक छ", + "type": "अमान्य मान प्रकार", + "enum": "अनुमति दिइएको मानहरू मध्ये एक हुनुपर्छ", + "const": "मान अपेक्षित स्थिरांकसँग मेल खाँदैन", + "uniqueItems": "सबै वस्तुहरू अद्वितीय हुनुपर्छ" } diff --git a/web/public/locales/ne/objects.json b/web/public/locales/ne/objects.json index 74f59f8ad5..e1826aaad9 100644 --- a/web/public/locales/ne/objects.json +++ b/web/public/locales/ne/objects.json @@ -3,5 +3,14 @@ "bicycle": "साइकल", "car": "कार", "motorcycle": "मोटरसाइकल", - "airplane": "हवाइजहाज" + "airplane": "हवाइजहाज", + "bus": "बस", + "train": "रेल", + "boat": "डुङ्गा", + "traffic_light": "ट्राफिक लाइट", + "fire_hydrant": "आगो निभाउने यन्त्र", + "street_sign": "सडक चिन्ह", + "stop_sign": "रोक चिन्ह", + "parking_meter": "पार्किङ मिटर", + "bench": "बेन्च" } diff --git a/web/public/locales/ne/views/chat.json b/web/public/locales/ne/views/chat.json index 774d1fcdb2..1afa99cc5e 100644 --- a/web/public/locales/ne/views/chat.json +++ b/web/public/locales/ne/views/chat.json @@ -3,5 +3,13 @@ "title": "फ्रिगेट च्याट", "subtitle": "क्यामेरा व्यवस्थापन र अन्तर्दृष्टिको लागि तपाईंको एआई सहायक", "placeholder": "सोध्नुहोस्...", - "error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।" + "error": "केही गडबड भयो। कृपया फेरि प्रयास गर्नुहोस्।", + "processing": "प्रशोधन गर्दै...", + "toolsUsed": "प्रयोग गरिएको: {{tools}}", + "showTools": "उपकरणहरू देखाउनुहोस् ({{count}})", + "hideTools": "उपकरणहरू लुकाउनुहोस्", + "call": "कल गर्नुहोस्", + "result": "नतिजा", + "arguments": "तर्कहरू:", + "response": "प्रतिक्रिया:" } diff --git a/web/public/locales/ne/views/classificationModel.json b/web/public/locales/ne/views/classificationModel.json index 4b42e1ec57..e37b8ee94c 100644 --- a/web/public/locales/ne/views/classificationModel.json +++ b/web/public/locales/ne/views/classificationModel.json @@ -10,6 +10,16 @@ }, "button": { "deleteClassificationAttempts": "वर्गीकरण छविहरू मेटाउनुहोस्", - "renameCategory": "वर्गको नाम बदल्नुहोस्" + "renameCategory": "वर्गको नाम बदल्नुहोस्", + "deleteCategory": "कक्षा मेटाउनुहोस्", + "deleteImages": "छविहरू मेटाउनुहोस्", + "trainModel": "रेल मोडेल", + "addClassification": "वर्गीकरण थप्नुहोस्", + "deleteModels": "मोडेलहरू मेटाउनुहोस्", + "editModel": "मोडेल सम्पादन गर्नुहोस्" + }, + "tooltip": { + "trainingInProgress": "मोडेल हाल प्रशिक्षणमा छिन्", + "noNewImages": "तालिम दिनको लागि कुनै नयाँ तस्बिरहरू छैनन्। पहिले डेटासेटमा थप तस्बिरहरू वर्गीकृत गर्नुहोस्।" } } diff --git a/web/public/locales/ne/views/configEditor.json b/web/public/locales/ne/views/configEditor.json index fb7d183536..ac03412677 100644 --- a/web/public/locales/ne/views/configEditor.json +++ b/web/public/locales/ne/views/configEditor.json @@ -3,5 +3,16 @@ "configEditor": "कन्फिग सम्पादक", "safeConfigEditor": "कन्फिग सम्पादक (सुरक्षित मोड)", "safeModeDescription": "कन्फिग प्रमाणीकरण त्रुटिको कारणले फ्रिगेट सुरक्षित मोडमा छ।", - "copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्" + "copyConfig": "कन्फिग प्रतिलिपि गर्नुहोस्", + "saveAndRestart": "बचत गर्नुहोस् र पुन: सुरु गर्नुहोस्", + "saveOnly": "बचत मात्र", + "confirm": "बचत नगरी बाहिर निस्कने हो?", + "toast": { + "success": { + "copyToClipboard": "कन्फिगरेसन क्लिपबोर्डमा प्रतिलिपि गरियो।" + }, + "error": { + "savingError": "कन्फिगरेसन बचत गर्दा त्रुटि भयो" + } + } } diff --git a/web/public/locales/ne/views/events.json b/web/public/locales/ne/views/events.json index 9dcd78594a..b04c4d7f7b 100644 --- a/web/public/locales/ne/views/events.json +++ b/web/public/locales/ne/views/events.json @@ -5,5 +5,19 @@ "label": "गति", "only": "गति मात्र" }, - "allCameras": "सबै क्यामेराहरू" + "allCameras": "सबै क्यामेराहरू", + "empty": { + "alert": "समीक्षा गर्न कुनै अलर्टहरू छैनन्", + "detection": "समीक्षा गर्न कुनै पनि पत्ता लगाइएको छैन", + "motion": "गतिसम्बन्धी कुनै डेटा फेला परेन", + "recordingsDisabled": { + "title": "रेकर्डिङहरू सक्षम पारिएको हुनुपर्छ", + "description": "क्यामेराको लागि रेकर्डिङ सक्षम पारिएको बेला मात्र समीक्षा वस्तुहरू सिर्जना गर्न सकिन्छ।" + } + }, + "timeline": { + "label": "समयरेखा", + "aria": "टाइमलाइन चयन गर्नुहोस्" + }, + "zoomIn": "जुम इन गर्नुहोस्" } diff --git a/web/public/locales/ne/views/explore.json b/web/public/locales/ne/views/explore.json index 815bcd764b..80ca127f6f 100644 --- a/web/public/locales/ne/views/explore.json +++ b/web/public/locales/ne/views/explore.json @@ -1,5 +1,28 @@ { "details": { "timestamp": "टाइमस्ट्याम्प" + }, + "documentTitle": "अन्वेषण गर्नुहोस् - फ्रिगेट", + "generativeAI": "जेनेरेटिभ एआई", + "exploreMore": "थप {{label}} वस्तुहरू अन्वेषण गर्नुहोस्", + "exploreIsUnavailable": { + "title": "अन्वेषण उपलब्ध छैन", + "embeddingsReindexing": { + "context": "ट्र्याक गरिएका वस्तु इम्बेडिङहरूले पुन: अनुक्रमणिका समाप्त गरेपछि अन्वेषण प्रयोग गर्न सकिन्छ।", + "startingUp": "सुरु गर्दै…", + "estimatedTime": "अनुमानित बाँकी समय:", + "finishingShortly": "चाँडै नै समाप्त हुँदैछ", + "step": { + "thumbnailsEmbedded": "इम्बेड गरिएका थम्बनेलहरू: ", + "descriptionsEmbedded": "इम्बेड गरिएका विवरणहरू: ", + "trackedObjectsProcessed": "ट्र्याक गरिएका वस्तुहरू प्रशोधन गरियो: " + } + }, + "downloadingModels": { + "context": "फ्रिगेटले सिमान्टिक खोज सुविधालाई समर्थन गर्न आवश्यक इम्बेडिङ मोडेलहरू डाउनलोड गर्दैछ। तपाईंको नेटवर्क जडानको गतिमा निर्भर गर्दै यसले धेरै मिनेट लिन सक्छ।", + "setup": { + "visionModel": "भिजन मोडेल" + } + } } } diff --git a/web/public/locales/ne/views/exports.json b/web/public/locales/ne/views/exports.json index e9d15ea91d..2afa762d39 100644 --- a/web/public/locales/ne/views/exports.json +++ b/web/public/locales/ne/views/exports.json @@ -4,5 +4,21 @@ "headings": { "cases": "केसहरू", "uncategorizedExports": "वर्गीकृत नगरिएका निर्यातहरू" + }, + "documentTitle": "निर्यात - फ्रिगेट", + "deleteExport": { + "label": "निर्यात मेटाउनुहोस्", + "desc": "के तपाईं {{exportName}} मेटाउन चाहनुहुन्छ?" + }, + "editExport": { + "title": "निर्यातको नाम बदल्नुहोस्", + "desc": "यो निर्यातको लागि नयाँ नाम प्रविष्ट गर्नुहोस्।", + "saveExport": "निर्यात बचत गर्नुहोस्" + }, + "tooltip": { + "shareExport": "निर्यात सेयर गर्नुहोस्", + "downloadVideo": "भिडियो डाउनलोड गर्नुहोस्", + "editName": "नाम सम्पादन गर्नुहोस्", + "deleteExport": "निर्यात मेटाउनुहोस्" } } diff --git a/web/public/locales/ne/views/faceLibrary.json b/web/public/locales/ne/views/faceLibrary.json index 67b7c6a9d5..1728f993cd 100644 --- a/web/public/locales/ne/views/faceLibrary.json +++ b/web/public/locales/ne/views/faceLibrary.json @@ -7,6 +7,20 @@ }, "details": { "unknown": "अज्ञात", - "timestamp": "टाइमस्ट्याम्प" + "timestamp": "टाइमस्ट्याम्प", + "scoreInfo": "स्कोर भनेको सबै अनुहारको स्कोरको भारित औसत हो, जुन प्रत्येक छविमा अनुहारको आकारद्वारा भारित हुन्छ।" + }, + "documentTitle": "फेस लाइब्रेरी - फ्रिगेट", + "uploadFaceImage": { + "title": "अनुहारको छवि अपलोड गर्नुहोस्", + "desc": "अनुहारहरू स्क्यान गर्न र {{pageToggle}} को लागि समावेश गर्न एउटा छवि अपलोड गर्नुहोस्" + }, + "collections": "सङ्ग्रहहरू", + "createFaceLibrary": { + "new": "नयाँ अनुहार सिर्जना गर्नुहोस्", + "nextSteps": "बलियो जग निर्माण गर्न:
  • प्रत्येक पत्ता लागेको व्यक्तिको लागि छविहरू चयन गर्न र तालिम दिन हालसालैको पहिचान ट्याब प्रयोग गर्नुहोस्।
  • उत्तम परिणामहरूको लागि सिधा-अन छविहरूमा ध्यान केन्द्रित गर्नुहोस्; कोणमा अनुहारहरू खिच्ने तालिम छविहरूबाट बच्नुहोस्।
  • " + }, + "steps": { + "faceName": "अनुहारको नाम प्रविष्ट गर्नुहोस्" } } diff --git a/web/public/locales/ne/views/live.json b/web/public/locales/ne/views/live.json index 6e2b6dbac9..2a4a191d62 100644 --- a/web/public/locales/ne/views/live.json +++ b/web/public/locales/ne/views/live.json @@ -7,5 +7,28 @@ "twoWayTalk": { "enable": "दुईतर्फी कुराकानी सक्षम पार्नुहोस्", "disable": "दुईतर्फी कुराकानी असक्षम पार्नुहोस्" + }, + "cameraAudio": { + "enable": "क्यामेरा अडियो सक्षम पार्नुहोस्", + "disable": "क्यामेरा अडियो असक्षम पार्नुहोस्" + }, + "ptz": { + "move": { + "clickMove": { + "label": "क्यामेरालाई केन्द्रमा राख्न फ्रेममा क्लिक गर्नुहोस्", + "enable": "सार्न क्लिक गर्नुहोस् सक्षम पार्नुहोस्", + "enableWithZoom": "सार्न क्लिक गर्नुहोस् / जुम गर्न तान्नुहोस् सक्षम गर्नुहोस्", + "disable": "सार्न क्लिक गर्ने सुविधा असक्षम पार्नुहोस्" + }, + "left": { + "label": "PTZ क्यामेरालाई बायाँतिर सार्नुहोस्" + }, + "up": { + "label": "PTZ क्यामेरा माथि सार्नुहोस्" + }, + "down": { + "label": "PTZ क्यामेरा तल सार्नुहोस्" + } + } } } diff --git a/web/public/locales/ne/views/motionSearch.json b/web/public/locales/ne/views/motionSearch.json index 1cae42fd4c..22533b1412 100644 --- a/web/public/locales/ne/views/motionSearch.json +++ b/web/public/locales/ne/views/motionSearch.json @@ -3,5 +3,15 @@ "title": "गति खोज", "description": "रुचिको क्षेत्र परिभाषित गर्न बहुभुज कोर्नुहोस्, र त्यो क्षेत्र भित्र गति परिवर्तनहरू खोज्नको लागि समय दायरा निर्दिष्ट गर्नुहोस्।", "selectCamera": "गति खोज लोड हुँदैछ", - "startSearch": "खोज सुरु गर्नुहोस्" + "startSearch": "खोज सुरु गर्नुहोस्", + "searchStarted": "खोजी सुरु भयो", + "searchCancelled": "खोज रद्द गरियो", + "cancelSearch": "रद्द गर्नुहोस्", + "searching": "खोजी भइरहेको छ।", + "searchComplete": "खोज पूरा भयो", + "noResultsYet": "चयन गरिएको क्षेत्रमा चाल परिवर्तनहरू फेला पार्न खोज चलाउनुहोस्", + "noChangesFound": "चयन गरिएको क्षेत्रमा कुनै पिक्सेल परिवर्तनहरू फेला परेनन्", + "changesFound_one": "{{count}} गति परिवर्तन फेला पर्यो", + "changesFound_other": "{{count}} गति परिवर्तनहरू फेला परे", + "framesProcessed": "{{count}} फ्रेमहरू प्रशोधन गरियो" } diff --git a/web/public/locales/ne/views/recording.json b/web/public/locales/ne/views/recording.json index 439507f27d..03ee1d4b3b 100644 --- a/web/public/locales/ne/views/recording.json +++ b/web/public/locales/ne/views/recording.json @@ -5,7 +5,8 @@ "filters": "फिल्टरहरू", "toast": { "error": { - "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन" + "noValidTimeSelected": "कुनै मान्य समय दायरा चयन गरिएको छैन", + "endTimeMustAfterStartTime": "अन्त्य समय सुरु समय पछि हुनुपर्छ" } } } diff --git a/web/public/locales/ne/views/replay.json b/web/public/locales/ne/views/replay.json index 320d74c42f..a9185bb087 100644 --- a/web/public/locales/ne/views/replay.json +++ b/web/public/locales/ne/views/replay.json @@ -5,6 +5,16 @@ "dialog": { "title": "डिबग रिप्ले सुरु गर्नुहोस्", "description": "वस्तु पत्ता लगाउने र ट्र्याकिङ समस्याहरू डिबग गर्न ऐतिहासिक फुटेज लुप गर्ने अस्थायी रिप्ले क्यामेरा सिर्जना गर्नुहोस्। रिप्ले क्यामेरामा स्रोत क्यामेरा जस्तै पत्ता लगाउने कन्फिगरेसन हुनेछ। सुरु गर्न समय दायरा छनौट गर्नुहोस्।", - "camera": "स्रोत क्यामेरा" + "camera": "स्रोत क्यामेरा", + "timeRange": "समय दायरा", + "preset": { + "1m": "अन्तिम १ मिनेट", + "5m": "अन्तिम ५ मिनेट", + "timeline": "टाइमलाइनबाट", + "custom": "अनुकूलन" + }, + "startButton": "रिप्ले सुरु गर्नुहोस्", + "selectFromTimeline": "चयन गर्नुहोस्", + "starting": "रिप्ले सुरु गर्दै..." } } diff --git a/web/public/locales/ne/views/search.json b/web/public/locales/ne/views/search.json index 065d64d379..185843f08a 100644 --- a/web/public/locales/ne/views/search.json +++ b/web/public/locales/ne/views/search.json @@ -4,6 +4,19 @@ "searchFor": "खोज्नुहोस् {{inputValue}}", "button": { "clear": "खोज खाली गर्नुहोस्", - "save": "खोज बचत गर्नुहोस्" + "save": "खोज बचत गर्नुहोस्", + "delete": "सुरक्षित गरिएको खोज मेटाउनुहोस्", + "filterInformation": "फिल्टर जानकारी", + "filterActive": "फिल्टरहरू सक्रिय छन्" + }, + "trackedObjectId": "ट्र्याक गरिएको वस्तु ID", + "filter": { + "label": { + "cameras": "क्यामेराहरू", + "labels": "लेबलहरू", + "zones": "क्षेत्रहरू", + "sub_labels": "उप लेबलहरू", + "attributes": "विशेषताहरू" + } } } diff --git a/web/public/locales/ne/views/settings.json b/web/public/locales/ne/views/settings.json index 6f7076bc20..0ce812e838 100644 --- a/web/public/locales/ne/views/settings.json +++ b/web/public/locales/ne/views/settings.json @@ -4,6 +4,14 @@ "authentication": "प्रमाणीकरण सेटिङहरू - फ्रिगेट", "cameraManagement": "क्यामेराहरू व्यवस्थापन गर्नुहोस् - फ्रिगेट", "cameraReview": "क्यामेरा समीक्षा सेटिङहरू - फ्रिगेट", - "enrichments": "संवर्धन सेटिङहरू - फ्रिगेट" + "enrichments": "संवर्धन सेटिङहरू - फ्रिगेट", + "masksAndZones": "मास्क र जोन सम्पादक - फ्रिगेट", + "motionTuner": "मोशन ट्युनर - फ्रिगेट", + "object": "डिबग - फ्रिगेट", + "general": "UI सेटिङहरू - फ्रिगेट", + "globalConfig": "विश्वव्यापी कन्फिगरेसन - फ्रिगेट", + "cameraConfig": "क्यामेरा कन्फिगरेसन - फ्रिगेट", + "frigatePlus": "फ्रिगेट+ सेटिङहरू - फ्रिगेट", + "notifications": "सूचना सेटिङहरू - फ्रिगेट" } } diff --git a/web/public/locales/ne/views/system.json b/web/public/locales/ne/views/system.json index 86c449ac83..e399283e5f 100644 --- a/web/public/locales/ne/views/system.json +++ b/web/public/locales/ne/views/system.json @@ -6,7 +6,19 @@ "enrichments": "संवर्धन तथ्याङ्क - फ्रिगेट", "logs": { "frigate": "फ्रिगेट लगहरू - फ्रिगेट", - "go2rtc": "Go2RTC लगहरू - फ्रिगेट" + "go2rtc": "Go2RTC लगहरू - फ्रिगेट", + "nginx": "Nginx लगहरू - फ्रिगेट", + "websocket": "सन्देश लगहरू - फ्रिगेट" + } + }, + "title": "प्रणाली", + "metrics": "प्रणाली मेट्रिक्स", + "logs": { + "websocket": { + "label": "सन्देशहरू", + "pause": "पज गर्नुहोस्", + "resume": "पुनःसुरु गर्नुहोस्", + "clear": "खाली गर्नुहोस्" } } } From f96127c2644b697be4cfafe575374ec01d790c9f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:56 +0200 Subject: [PATCH 22/94] Translated using Weblate (Spanish) Currently translated at 100.0% (53 of 53 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1171 of 1171 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (811 of 811 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1150 of 1150 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1137 of 1137 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (175 of 175 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1129 of 1129 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (145 of 145 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (129 of 129 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (45 of 45 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (59 of 59 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (64 of 64 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (86 of 86 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (40 of 40 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (471 of 471 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (101 of 101 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (1122 of 1122 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (792 of 792 strings) Translated using Weblate (Spanish) Currently translated at 52.5% (31 of 59 strings) Translated using Weblate (Spanish) Currently translated at 99.4% (174 of 175 strings) Translated using Weblate (Spanish) Currently translated at 23.3% (110 of 471 strings) Translated using Weblate (Spanish) Currently translated at 68.8% (31 of 45 strings) Translated using Weblate (Spanish) Currently translated at 21.8% (173 of 792 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (100 of 100 strings) Translated using Weblate (Spanish) Currently translated at 62.3% (63 of 101 strings) Translated using Weblate (Spanish) Currently translated at 40.6% (35 of 86 strings) Translated using Weblate (Spanish) Currently translated at 80.0% (32 of 40 strings) Translated using Weblate (Spanish) Currently translated at 67.6% (759 of 1122 strings) Translated using Weblate (Spanish) Currently translated at 70.3% (45 of 64 strings) Co-authored-by: Hosted Weblate Co-authored-by: jjavin Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/es/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/components-dialog Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-classificationmodel Translation: Frigate NVR/views-events Translation: Frigate NVR/views-explore Translation: Frigate NVR/views-exports Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/es/common.json | 29 +- web/public/locales/es/components/dialog.json | 74 +- web/public/locales/es/config/cameras.json | 774 +++++++++- web/public/locales/es/config/global.json | 1371 +++++++++++++++-- web/public/locales/es/objects.json | 12 +- web/public/locales/es/views/chat.json | 70 +- .../locales/es/views/classificationModel.json | 2 +- web/public/locales/es/views/events.json | 29 +- web/public/locales/es/views/explore.json | 9 +- web/public/locales/es/views/exports.json | 87 +- web/public/locales/es/views/faceLibrary.json | 6 +- web/public/locales/es/views/live.json | 3 +- web/public/locales/es/views/motionSearch.json | 78 +- web/public/locales/es/views/replay.json | 60 +- web/public/locales/es/views/settings.json | 668 +++++++- web/public/locales/es/views/system.json | 8 +- 16 files changed, 3069 insertions(+), 211 deletions(-) diff --git a/web/public/locales/es/common.json b/web/public/locales/es/common.json index 8faa18fe75..b8b44f2c12 100644 --- a/web/public/locales/es/common.json +++ b/web/public/locales/es/common.json @@ -154,7 +154,8 @@ "gl": "Galego (Gallego)", "id": "Bahasa Indonesia (Indonesio)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Croata)" + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bosnio)" }, "appearance": "Apariencia", "darkMode": { @@ -196,7 +197,10 @@ "uiPlayground": "Zona de pruebas de la interfaz de usuario", "faceLibrary": "Biblioteca de rostros", "classification": "Clasificación", - "profiles": "Perfiles" + "profiles": "Perfiles", + "actions": "Acciones", + "features": "Funciones", + "chat": "Chat" }, "unit": { "speed": { @@ -252,7 +256,19 @@ "saving": "Guardando…", "exitFullscreen": "Salir de pantalla completa", "on": "ENCENDIDO", - "continue": "Continuar" + "continue": "Continuar", + "add": "Añadir", + "applying": "Aplicando…", + "undo": "Deshacer", + "copiedToClipboard": "Copiado al portapapeles", + "modified": "Modificado", + "overridden": "Sobrescrito", + "resetToGlobal": "Restablecer a global", + "resetToDefault": "Restablecer valores predeterminados", + "saveAll": "Guardar todo", + "savingAll": "Guardando todo…", + "undoAll": "Deshacer todo", + "retry": "Reintentar" }, "toast": { "save": { @@ -260,7 +276,8 @@ "noMessage": "No se pudieron guardar los cambios de configuración", "title": "No se pudieron guardar los cambios de configuración: {{errorMessage}}" }, - "title": "Guardar" + "title": "Guardar", + "success": "Cambios de configuración guardados correctamente." }, "copyUrlToClipboard": "URL copiada al portapapeles." }, @@ -314,5 +331,7 @@ "field": { "optional": "Opcional", "internalID": "La ID interna que usa Frigate en la configuración y en la base de datos" - } + }, + "no_items": "No hay elementos", + "validation_errors": "Errores de validación" } diff --git a/web/public/locales/es/components/dialog.json b/web/public/locales/es/components/dialog.json index 848285dafa..07d7862b66 100644 --- a/web/public/locales/es/components/dialog.json +++ b/web/public/locales/es/components/dialog.json @@ -71,16 +71,77 @@ "endTimeMustAfterStartTime": "La hora de finalización debe ser posterior a la hora de inicio" }, "success": "Exportación iniciada con éxito. Ver el archivo en la página exportaciones.", - "view": "Ver" + "view": "Ver", + "queued": "Exportación en cola. Consulta el progreso en la página de exportaciones.", + "batchSuccess_one": "Se inició 1 exportación. Abriendo el caso ahora.", + "batchSuccess_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "batchSuccess_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "batchPartial": "Se iniciaron {{successful}} de {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}", + "batchFailed": "No se pudieron iniciar {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}", + "batchQueuedSuccess_one": "1 exportación en cola. Abriendo el caso ahora.", + "batchQueuedSuccess_many": "{{count}} exportaciones en cola. Abriendo el caso ahora.", + "batchQueuedSuccess_other": "{{count}} exportaciones en cola. Abriendo el caso ahora.", + "batchQueuedPartial": "{{successful}} de {{total}} exportaciones en cola. Cámaras fallidas: {{failedCameras}}", + "batchQueueFailed": "No se pudieron poner en cola {{total}} exportaciones. Cámaras fallidas: {{failedCameras}}" }, "fromTimeline": { "saveExport": "Guardar exportación", - "previewExport": "Vista previa de la exportación" + "previewExport": "Vista previa de la exportación", + "queueingExport": "Poniendo exportación en cola...", + "useThisRange": "Usar este intervalo" }, "selectOrExport": "Seleccionar o exportar", "case": { "label": "Caso", - "newCaseDescriptionPlaceholder": "Descripción de caso" + "newCaseDescriptionPlaceholder": "Descripción de caso", + "newCaseOption": "Crear nuevo caso", + "newCaseNamePlaceholder": "Nombre del nuevo caso", + "nonAdminHelp": "Se creará un nuevo caso para estas exportaciones.", + "placeholder": "Selecciona un caso" + }, + "queueing": "Poniendo la exportación en cola…", + "tabs": { + "export": "Cámara única", + "multiCamera": "Multicámara" + }, + "multiCamera": { + "timeRange": "Intervalo de tiempo", + "selectFromTimeline": "Seleccionar desde la línea de tiempo", + "cameraSelection": "Cámaras", + "cameraSelectionHelp": "Las cámaras con objetos detectados en este intervalo de tiempo están preseleccionadas", + "checkingActivity": "Comprobando actividad de las cámaras...", + "noCameras": "No hay cámaras disponibles", + "detectionCount_one": "1 objeto detectado", + "detectionCount_many": "{{count}} objetos detectados", + "detectionCount_other": "{{count}} objetos detectados", + "nameLabel": "Nombre de la exportación", + "namePlaceholder": "Nombre base opcional para estas exportaciones", + "queueingButton": "Poniendo exportaciones en cola...", + "exportButton_one": "Exportar 1 cámara", + "exportButton_many": "Exportar {{count}} cámaras", + "exportButton_other": "Exportar {{count}} cámaras" + }, + "multi": { + "title_one": "Exportar 1 revisión", + "title_many": "Exportar {{count}} revisiones", + "title_other": "Exportar {{count}} revisiones", + "description": "Exportar cada revisión seleccionada. Todas las exportaciones se agruparán en un único caso.", + "descriptionNoCase": "Exportar cada revisión seleccionada.", + "caseNamePlaceholder": "Exportación de revisión - {{date}}", + "exportButton_one": "Exportar 1 revisión", + "exportButton_many": "Exportar {{count}} revisiones", + "exportButton_other": "Exportar {{count}} revisiones", + "exportingButton": "Exportando...", + "toast": { + "started_one": "Se inició 1 exportación. Abriendo el caso ahora.", + "started_many": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "started_other": "Se iniciaron {{count}} exportaciones. Abriendo el caso ahora.", + "startedNoCase_one": "Se inició 1 exportación.", + "startedNoCase_many": "Se iniciaron {{count}} exportaciones.", + "startedNoCase_other": "Se iniciaron {{count}} exportaciones.", + "partial": "Se iniciaron {{successful}} de {{total}} exportaciones. Fallidas: {{failedItems}}", + "failed": "No se pudieron iniciar {{total}} exportaciones. Fallidas: {{failedItems}}" + } } }, "streaming": { @@ -130,7 +191,12 @@ "markAsUnreviewed": "Marcar como no revisado" }, "shareTimestamp": { - "description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara." + "description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara.", + "label": "Compartir marca de tiempo", + "title": "Compartir marca de tiempo", + "custom": "Marca de tiempo personalizada", + "button": "Compartir URL de la marca de tiempo", + "shareTitle": "Marca de tiempo de revisión de Frigate: {{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/es/config/cameras.json b/web/public/locales/es/config/cameras.json index d6a120fdfd..ae0b7437fd 100644 --- a/web/public/locales/es/config/cameras.json +++ b/web/public/locales/es/config/cameras.json @@ -8,7 +8,7 @@ "description": "Habilitado" }, "audio": { - "label": "Eventos de audio", + "label": "Detección de audio", "description": "Configuración para la detección de eventos basada en audio para esta cámara.", "enabled": { "label": "Habilitar la detección de audio", @@ -28,14 +28,19 @@ }, "filters": { "label": "Filtros de audio", - "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos." + "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.", + "threshold": { + "label": "Confianza mínima de audio", + "description": "Umbral mínimo de confianza para que se cuente el evento de audio." + } }, "enabled_in_config": { "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estática.", "label": "Estado original del audio" }, "num_threads": { - "label": "Hilos de detección" + "label": "Hilos de detección", + "description": "Número de hilos que se utilizarán para el procesamiento de la detección de audio." } }, "friendly_name": { @@ -50,29 +55,79 @@ }, "autotracking": { "zoom_factor": { - "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75." + "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.", + "label": "Factor de zoom" }, "calibrate_on_startup": { - "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración." + "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.", + "label": "Calibrar al iniciar" }, "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.", "zooming": { - "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos)." + "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).", + "label": "Modo de zoom" }, "return_preset": { - "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento." + "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.", + "label": "Preajuste de retorno" }, "timeout": { - "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida." + "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.", + "label": "Tiempo de espera de retorno" + }, + "label": "Seguimiento automático", + "enabled": { + "label": "Habilitar seguimiento automático", + "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados." + }, + "track": { + "label": "Objetos rastreados", + "description": "Lista de tipos de objetos que deben activar el seguimiento automático." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático." + }, + "movement_weights": { + "label": "Pesos de movimiento", + "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente." + }, + "enabled_in_config": { + "label": "Estado original de autoseguimiento", + "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración." } }, "tls_insecure": { - "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras)." + "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).", + "label": "Deshabilitar verificación TLS" + }, + "label": "ONVIF", + "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.", + "host": { + "label": "Host ONVIF", + "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara." + }, + "port": { + "label": "Puerto ONVIF", + "description": "Número de puerto del servicio ONVIF." + }, + "user": { + "label": "Nombre de usuario ONVIF", + "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF." + }, + "password": { + "label": "Contraseña ONVIF", + "description": "Contraseña para la autenticación ONVIF." + }, + "ignore_time_mismatch": { + "label": "Ignorar discrepancia horaria", + "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF." } }, "zones": { "distances": { - "label": "Distancias reales" + "label": "Distancias reales", + "description": "Distancias reales opcionales para cada lado del cuadrilátero de la zona, usadas para cálculos de velocidad o distancia. Debe tener exactamente 4 valores si se establece." }, "coordinates": { "description": "Coordenadas del polígono que definen el área de la zona. Puede ser una cadena separada por comas o una lista de cadenas de coordenadas. Las coordenadas deben ser relativas (0-1) o absolutas (heredadas).", @@ -106,23 +161,41 @@ "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede expresarse en píxeles (entero) o como porcentaje (decimal entre 0,000001 y 0,99).", "label": "Área máxima del objeto" }, - "description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona." + "description": "Filtros para aplicar a los objetos dentro de esta zona. Se utilizan para reducir los falsos positivos o restringir qué objetos se consideran presentes en la zona.", + "label": "Filtros de zona", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + } }, "objects": { - "description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos." + "description": "Lista de tipos de objetos (del mapa de etiquetas) que pueden activar esta zona. Puede ser una cadena de texto o una lista de cadenas. Si está vacío, se consideran todos los objetos.", + "label": "Objetos activadores" }, "description": "Las zonas le permiten definir un área específica del fotograma, de modo que pueda determinar si un objeto se encuentra o no dentro de un área determinada.", "speed_threshold": { - "description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad." + "description": "Velocidad mínima (en unidades del mundo real, si se han configurado distancias) requerida para que un objeto se considere presente en la zona. Se utiliza para los disparadores de zona basados en la velocidad.", + "label": "Velocidad mínima" }, "friendly_name": { - "description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona." + "description": "Un nombre fácil de usar para la zona, que se muestra en la interfaz de usuario de Frigate. Si no se especifica, se utilizará una versión formateada del nombre de la zona.", + "label": "Nombre de zona" }, "inertia": { - "description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias." + "description": "Número de fotogramas consecutivos en los que se debe detectar un objeto dentro de la zona antes de considerarlo presente. Ayuda a filtrar las detecciones transitorias.", + "label": "Fotogramas de inercia" }, "loitering_time": { - "description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo." + "description": "Número de segundos que un objeto debe permanecer en la zona para ser considerado como merodeo. Establezca en 0 para desactivar la detección de merodeo.", + "label": "Segundos de permanencia" + }, + "label": "Zonas", + "enabled": { + "label": "Habilitado", + "description": "Habilita o deshabilita esta zona. Las zonas deshabilitadas se ignoran en tiempo de ejecución." + }, + "enabled_in_config": { + "label": "Mantiene el registro del estado original de la zona." } }, "objects": { @@ -142,148 +215,739 @@ }, "send_triggers": { "after_significant_updates": { - "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado." + "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.", + "label": "Activador temprano de GenAI" }, - "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.)." + "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).", + "label": "Activadores de GenAI", + "tracked_object_end": { + "label": "Enviar al finalizar", + "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado." + } }, "required_zones": { - "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI." + "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.", + "label": "Zonas requeridas" + }, + "prompt": { + "label": "Prompt de descripción", + "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI." + }, + "object_prompts": { + "label": "Prompts de objetos", + "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada." + }, + "debug_save_thumbnails": { + "label": "Guardar miniaturas", + "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Indica si GenAI estaba habilitado en la configuración estática original." } + }, + "label": "Objetos", + "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.", + "track": { + "label": "Objetos a rastrear", + "description": "Lista de etiquetas de objetos a rastrear para esta cámara." + }, + "filters": { + "label": "Filtros de objetos", + "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima de objeto", + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas." } }, "mqtt": { "label": "MQTT", "required_zones": { - "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT." + "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.", + "label": "Zonas requeridas" + }, + "description": "Ajustes de publicación de imágenes MQTT.", + "enabled": { + "label": "Enviar imagen", + "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara." + }, + "timestamp": { + "label": "Añadir marca de tiempo", + "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT." + }, + "bounding_box": { + "label": "Añadir cuadro delimitador", + "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT." + }, + "crop": { + "label": "Recortar imagen", + "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado." + }, + "height": { + "label": "Altura de imagen", + "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT." + }, + "quality": { + "label": "Calidad JPEG", + "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)." } }, "notifications": { "email": { - "label": "Email de notificacion" + "label": "Email de notificacion", + "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones." + }, + "label": "Notificaciones", + "description": "Ajustes para habilitar y controlar las notificaciones de esta cámara.", + "enabled": { + "label": "Habilitar notificaciones", + "description": "Habilita o deshabilita las notificaciones para esta cámara." + }, + "cooldown": { + "label": "Periodo de enfriamiento", + "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios." + }, + "enabled_in_config": { + "label": "Estado original de notificaciones", + "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original." } }, "audio_transcription": { "description": "Configuración para la transcripción de audio en vivo y de voz, utilizada para eventos y subtítulos en tiempo real.", "enabled": { - "label": "Habilitar transcripción" + "label": "Habilitar transcripción", + "description": "Activar o desactivar la transcripción de eventos de audio activados manualmente." + }, + "label": "Transcripción de audio", + "enabled_in_config": { + "label": "Estado original de la transcripción" + }, + "live_enabled": { + "label": "Transcripción en directo", + "description": "Activar la transcripción en directo del audio a medida que se recibe." } }, "motion": { "skip_motion_threshold": { - "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función." + "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.", + "label": "Omitir umbral de movimiento" }, "lightning_threshold": { - "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento." + "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.", + "label": "Umbral de iluminación" }, "threshold": { - "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255)." + "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).", + "label": "Umbral de movimiento" + }, + "label": "Detección de movimiento", + "description": "Ajustes predeterminados de detección de movimiento para esta cámara.", + "enabled": { + "label": "Habilitar detección de movimiento", + "description": "Habilita o deshabilita la detección de movimiento para esta cámara." + }, + "improve_contrast": { + "label": "Mejorar contraste", + "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento." + }, + "frame_alpha": { + "label": "Alfa del fotograma", + "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento." + }, + "frame_height": { + "label": "Altura del fotograma", + "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento." + }, + "mask": { + "label": "Coordenadas de máscara", + "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas." + }, + "mqtt_off_delay": { + "label": "Retraso de apagado MQTT", + "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'." + }, + "enabled_in_config": { + "label": "Estado de movimiento original", + "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original." + }, + "raw_mask": { + "label": "Máscara sin procesar" } }, "lpr": { "enhancement": { - "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución." + "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.", + "label": "Nivel de mejora" }, "expire_time": { - "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas)." + "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).", + "label": "Segundos hasta caducar" + }, + "label": "Reconocimiento de matrículas", + "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.", + "enabled": { + "label": "Habilitar LPR", + "description": "Habilita o deshabilita LPR en esta cámara." + }, + "min_area": { + "label": "Área mínima de matrícula", + "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento." } }, "detect": { "fps": { - "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez)." + "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).", + "label": "FPS de detección" }, "min_initialized": { - "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2." + "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.", + "label": "Fotogramas mínimos de inicialización" }, "height": { - "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión." + "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Altura de detección" }, "width": { - "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión." + "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Anchura de detección" }, "stationary": { - "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo." + "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.", + "label": "Configuración de objetos estacionarios", + "interval": { + "label": "Intervalo estacionario", + "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario." + }, + "threshold": { + "label": "Umbral estacionario", + "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario." + }, + "max_frames": { + "label": "Fotogramas máximos", + "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.", + "default": { + "label": "Fotogramas máximos predeterminados", + "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse." + }, + "objects": { + "label": "Fotogramas máximos por objeto", + "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios." + } + }, + "classifier": { + "label": "Habilitar clasificador visual", + "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan." + } + }, + "label": "Detección de objetos", + "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.", + "enabled": { + "label": "Habilitar detección de objetos", + "description": "Habilita o deshabilita la detección de objetos para esta cámara." + }, + "max_disappeared": { + "label": "Fotogramas máximos desaparecido", + "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido." + }, + "annotation_offset": { + "label": "Desplazamiento de anotaciones", + "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo." } }, "record": { "motion": { - "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones." + "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención de movimiento", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } }, "continuous": { - "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones." + "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención continua", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } }, "detections": { "pre_capture": { - "description": "Número de segundos antes del evento de detección que se incluirán en la grabación." + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" }, "post_capture": { - "description": "Número de segundos después del evento de detección que se incluirán en la grabación." + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de detección", + "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } } }, "alerts": { "pre_capture": { - "description": "Número de segundos antes del evento de detección que se incluirán en la grabación." + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" }, "post_capture": { - "description": "Número de segundos después del evento de detección que se incluirán en la grabación." + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de alertas", + "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } } + }, + "label": "Grabación", + "description": "Ajustes de grabación y retención para esta cámara.", + "enabled": { + "label": "Habilitar grabación", + "description": "Habilita o deshabilita la grabación para esta cámara." + }, + "expire_interval": { + "label": "Intervalo de limpieza de grabaciones", + "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados." + }, + "export": { + "label": "Configuración de exportación", + "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.", + "hwaccel_args": { + "label": "Argumentos hwaccel de exportación", + "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación." + }, + "max_concurrent": { + "label": "Exportaciones simultáneas máximas", + "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo." + } + }, + "preview": { + "label": "Configuración de vista previa", + "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.", + "quality": { + "label": "Calidad de vista previa", + "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Estado de grabación original", + "description": "Indica si la grabación estaba habilitada en la configuración estática original." } }, "ui": { "dashboard": { - "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz." + "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.", + "label": "Mostrar en la interfaz" + }, + "label": "Interfaz de cámara", + "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.", + "order": { + "label": "Orden en la interfaz", + "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde." } }, "live": { "height": { - "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección." + "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.", + "label": "Altura en directo" }, - "description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo." + "description": "Configuraciones utilizadas por la interfaz web para controlar la selección, la resolución y la calidad de transmisiónes en vivo.", + "label": "Reproducción en directo", + "streams": { + "label": "Nombres de flujos en directo", + "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo." + }, + "quality": { + "label": "Calidad en directo", + "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)." + } }, "review": { "description": "Configuraciones que controlan las alertas, las detecciones y los resúmenes de revisión de GenAI utilizados por la interfaz de usuario y el almacenamiento de esta cámara.", "alerts": { "required_zones": { - "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona." + "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" }, "labels": { - "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person)." + "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).", + "label": "Etiquetas de alerta" + }, + "label": "Configuración de alertas", + "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.", + "enabled": { + "label": "Habilitar alertas", + "description": "Habilita o deshabilita la generación de alertas para esta cámara." + }, + "enabled_in_config": { + "label": "Estado original de alertas", + "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática." + }, + "cutoff_time": { + "label": "Tiempo de corte de alertas", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta." } }, "detections": { "required_zones": { - "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona." + "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" }, - "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones." + "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.", + "label": "Configuración de detecciones", + "enabled": { + "label": "Habilitar detecciones", + "description": "Habilita o deshabilita los eventos de detección para esta cámara." + }, + "labels": { + "label": "Etiquetas de detección", + "description": "Lista de etiquetas de objetos que cuentan como eventos de detección." + }, + "cutoff_time": { + "label": "Tiempo de corte de detecciones", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección." + }, + "enabled_in_config": { + "label": "Estado original de detecciones", + "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática." + } }, "genai": { "image_source": { - "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens." + "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.", + "label": "Origen de imagen de revisión" }, "additional_concerns": { - "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara." + "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.", + "label": "Consideraciones adicionales" }, "activity_context_prompt": { - "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI." + "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.", + "label": "Prompt de contexto de actividad" }, "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.", "debug_save_thumbnails": { - "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión." + "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.", + "label": "Guardar miniaturas" + }, + "label": "Configuración de GenAI", + "enabled": { + "label": "Habilitar descripciones de GenAI", + "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión." + }, + "alerts": { + "label": "Habilitar GenAI para alertas", + "description": "Usa GenAI para generar descripciones de elementos de alerta." + }, + "detections": { + "label": "Habilitar GenAI para detecciones", + "description": "Usa GenAI para generar descripciones de elementos de detección." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática." + }, + "preferred_language": { + "label": "Idioma preferido", + "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas." } - } + }, + "label": "Revisión" }, "birdseye": { - "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista." + "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.", + "label": "Vista general", + "enabled": { + "label": "Habilitar Birdseye", + "description": "Habilita o deshabilita la función de vista Birdseye." + }, + "mode": { + "label": "Modo de seguimiento", + "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'." + }, + "order": { + "label": "Posición", + "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye." + } }, "ffmpeg": { "retry_interval": { - "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10." + "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.", + "label": "Tiempo de reintento de FFmpeg" }, "path": { - "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\")." + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\").", + "label": "Ruta de FFmpeg" }, "output_args": { - "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación." + "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.", + "label": "Argumentos de salida", + "detect": { + "label": "Argumentos de salida de detección", + "description": "Argumentos de salida predeterminados para los flujos con rol de detección." + }, + "record": { + "label": "Argumentos de salida de grabación", + "description": "Argumentos de salida predeterminados para los flujos con rol de grabación." + } }, - "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol." + "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.", + "label": "FFmpeg", + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales pasados a los procesos de FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg." + }, + "apple_compatibility": { + "label": "Compatibilidad con Apple", + "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265." + }, + "gpu": { + "label": "Índice de GPU", + "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible." + }, + "inputs": { + "label": "Entradas de cámara", + "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.", + "path": { + "label": "Ruta de entrada", + "description": "URL o ruta del flujo de entrada de la cámara." + }, + "roles": { + "label": "Roles de entrada", + "description": "Roles para este flujo de entrada." + }, + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales de FFmpeg para este flujo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para este flujo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este flujo." + } + } + }, + "face_recognition": { + "label": "Reconocimiento facial", + "description": "Ajustes de detección y reconocimiento facial para esta cámara.", + "enabled": { + "label": "Habilitar reconocimiento facial", + "description": "Habilita o deshabilita el reconocimiento facial." + }, + "min_area": { + "label": "Área mínima de rostro", + "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento." + } + }, + "semantic_search": { + "label": "Búsqueda semántica", + "description": "Ajustes de búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.", + "triggers": { + "label": "Activadores", + "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador." + }, + "enabled": { + "label": "Habilitar este activador", + "description": "Habilita o deshabilita este activador de búsqueda semántica." + }, + "type": { + "label": "Tipo de activador", + "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)." + }, + "data": { + "label": "Contenido del activador", + "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados." + }, + "threshold": { + "label": "Umbral del activador", + "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador." + }, + "actions": { + "label": "Acciones del activador", + "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)." + } + } + }, + "snapshots": { + "label": "Instantáneas", + "description": "Ajustes de instantáneas generadas por la API de objetos rastreados para esta cámara.", + "enabled": { + "label": "Habilitar instantáneas", + "description": "Habilita o deshabilita el guardado de instantáneas para esta cámara." + }, + "timestamp": { + "label": "Superposición de marca de tiempo", + "description": "Superpone una marca de tiempo en las instantáneas de la API." + }, + "bounding_box": { + "label": "Superposición de cuadro delimitador", + "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API." + }, + "crop": { + "label": "Recortar instantánea", + "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea." + }, + "height": { + "label": "Altura de instantánea", + "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original." + }, + "retain": { + "label": "Retención de instantáneas", + "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.", + "default": { + "label": "Retención predeterminada", + "description": "Número predeterminado de días durante los que conservar instantáneas." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + }, + "objects": { + "label": "Retención por objeto", + "description": "Sobrescrituras por objeto para los días de retención de instantáneas." + } + }, + "quality": { + "label": "Calidad de instantánea", + "description": "Calidad de codificación de las instantáneas guardadas (0-100)." + } + }, + "timestamp_style": { + "label": "Estilo de marca de tiempo", + "description": "Opciones de estilo para marcas de tiempo integradas aplicadas a grabaciones e instantáneas.", + "position": { + "label": "Posición de marca de tiempo", + "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)." + }, + "format": { + "label": "Formato de marca de tiempo", + "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)." + }, + "color": { + "label": "Color de marca de tiempo", + "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).", + "red": { + "label": "Rojo", + "description": "Componente rojo (0-255) para el color de la marca de tiempo." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) para el color de la marca de tiempo." + }, + "blue": { + "label": "Azul", + "description": "Componente azul (0-255) para el color de la marca de tiempo." + } + }, + "thickness": { + "label": "Grosor de marca de tiempo", + "description": "Grosor de línea del texto de la marca de tiempo." + }, + "effect": { + "label": "Efecto de marca de tiempo", + "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)." + } + }, + "best_image_timeout": { + "label": "Tiempo de espera de mejor imagen", + "description": "Tiempo que se esperará la imagen con la puntuación de confianza más alta." + }, + "type": { + "label": "Tipo de cámara", + "description": "Tipo de cámara" + }, + "webui_url": { + "label": "URL de la cámara", + "description": "URL para visitar la cámara directamente desde la página del sistema" + }, + "profiles": { + "label": "Perfiles", + "description": "Perfiles de configuración con nombre y sobrescrituras parciales que pueden activarse en tiempo de ejecución." + }, + "enabled_in_config": { + "label": "Estado original de cámara", + "description": "Mantiene el registro del estado original de la cámara." } } diff --git a/web/public/locales/es/config/global.json b/web/public/locales/es/config/global.json index 1fc7df1ccd..a6931ba473 100644 --- a/web/public/locales/es/config/global.json +++ b/web/public/locales/es/config/global.json @@ -24,9 +24,10 @@ } }, "audio": { - "label": "Eventos de audio", + "label": "Detección de audio", "enabled": { - "label": "Habilitar la detección de audio" + "label": "Habilitar la detección de audio", + "description": "Habilita o deshabilita la detección de eventos de audio para todas las cámaras; se puede sobrescribir por cámara." }, "max_not_heard": { "label": "Finalizar el tiempo de espera", @@ -42,15 +43,21 @@ }, "filters": { "label": "Filtros de audio", - "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos." + "description": "Ajustes de filtrado por tipo de audio, como umbrales de confianza utilizados para reducir los falsos positivos.", + "threshold": { + "label": "Confianza mínima de audio", + "description": "Umbral mínimo de confianza para que se cuente el evento de audio." + } }, "enabled_in_config": { "description": "Indica si la detección de audio estaba habilitada originalmente en el archivo de configuración estática.", "label": "Estado original del audio" }, "num_threads": { - "label": "Hilos de detección" - } + "label": "Hilos de detección", + "description": "Número de hilos que se utilizarán para el procesamiento de la detección de audio." + }, + "description": "Ajustes para la detección de eventos basada en audio en todas las cámaras; se pueden sobrescribir por cámara." }, "auth": { "label": "Autenticación", @@ -72,16 +79,32 @@ "description": "Establece el flag de seguridad en la cookie de autenticación; debe ser 'true' cuando se utilice TLS." }, "failed_login_rate_limit": { - "label": "Limite de intento de acceso fallidos" + "label": "Limite de intento de acceso fallidos", + "description": "Reglas de limitación de intentos de inicio de sesión fallidos para reducir los ataques de fuerza bruta." }, "session_length": { - "description": "Duración de la sesión en segundos para sesiones de JWT." + "description": "Duración de la sesión en segundos para sesiones de JWT.", + "label": "Duración de la sesión" }, "admin_first_time_login": { - "description": "Cuando se establece en true, la interfaz de usuario puede mostrar un enlace de ayuda en la página de inicio de sesión, informando a los usuarios sobre cómo iniciar sesión tras el restablecimiento de la contraseña de administrador. " + "description": "Cuando se establece en true, la interfaz de usuario puede mostrar un enlace de ayuda en la página de inicio de sesión, informando a los usuarios sobre cómo iniciar sesión tras el restablecimiento de la contraseña de administrador. ", + "label": "Marca de administrador inicial" }, "refresh_time": { - "description": "Cuando a una sesión le queden menos de esta cantidad de segundos para expirar, actualícela para restablecer su duración completa." + "description": "Cuando a una sesión le queden menos de esta cantidad de segundos para expirar, actualícela para restablecer su duración completa.", + "label": "Ventana de actualización de la sesión" + }, + "trusted_proxies": { + "label": "Proxies de confianza", + "description": "Lista de IPs de proxies de confianza utilizadas para determinar la IP del cliente en la limitación de peticiones." + }, + "hash_iterations": { + "label": "Iteraciones de hash", + "description": "Número de iteraciones PBKDF2-SHA256 que se utilizarán al generar el hash de las contraseñas de los usuarios." + }, + "roles": { + "label": "Asignaciones de roles", + "description": "Asigna roles a listas de cámaras. Una lista vacía concede acceso a todas las cámaras para ese rol." } }, "onvif": { @@ -91,24 +114,73 @@ }, "autotracking": { "zoom_factor": { - "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75." + "description": "Controla el nivel de zoom en los objetos rastreados. Los valores más bajos mantienen una mayor parte de la escena a la vista; los valores más altos acercan la imagen, pero pueden provocar la pérdida del rastreo. Valores entre 0.1 y 0.75.", + "label": "Factor de zoom" }, "calibrate_on_startup": { - "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración." + "description": "Mida la velocidad de los motores PTZ al encenderlos para mejorar la precisión del seguimiento. Frigate actualizará la configuración con los `movement_weights` tras la calibración.", + "label": "Calibrar al iniciar" }, "description": "Realice un seguimiento automático de objetos en movimiento y manténgalos centrados en el encuadre mediante movimientos de cámara PTZ.", "zooming": { - "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos)." + "description": "Control del comportamiento del zoom: deshabilitado (solo panorámica/inclinación), absoluto (mayor compatibilidad) o relativo (panorámica/inclinación/zoom simultáneos).", + "label": "Modo de zoom" }, "return_preset": { - "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento." + "description": "Nombre del preajuste ONVIF configurado en el firmware de la cámara al que regresar una vez finalizado el seguimiento.", + "label": "Preajuste de retorno" }, "timeout": { - "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida." + "description": "Espere esta cantidad de segundos después de perder el seguimiento antes de devolver la cámara a la posición preestablecida.", + "label": "Tiempo de espera de retorno" + }, + "label": "Seguimiento automático", + "enabled": { + "label": "Habilitar seguimiento automático", + "description": "Habilita o deshabilita el seguimiento automático con cámara PTZ de objetos detectados." + }, + "track": { + "label": "Objetos rastreados", + "description": "Lista de tipos de objetos que deben activar el seguimiento automático." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Los objetos deben entrar en una de estas zonas antes de que comience el seguimiento automático." + }, + "movement_weights": { + "label": "Pesos de movimiento", + "description": "Valores de calibración generados automáticamente por la calibración de la cámara. No los modifiques manualmente." + }, + "enabled_in_config": { + "label": "Estado original de autoseguimiento", + "description": "Campo interno para rastrear si el seguimiento automático estaba habilitado en la configuración." } }, "tls_insecure": { - "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras)." + "description": "Omitir la verificación TLS y deshabilitar la autenticación digest para ONVIF (no seguro; usar solo en redes seguras).", + "label": "Deshabilitar verificación TLS" + }, + "label": "ONVIF", + "description": "Ajustes de conexión ONVIF y seguimiento automático PTZ para esta cámara.", + "host": { + "label": "Host ONVIF", + "description": "Host (y esquema opcional) para el servicio ONVIF de esta cámara." + }, + "port": { + "label": "Puerto ONVIF", + "description": "Número de puerto del servicio ONVIF." + }, + "user": { + "label": "Nombre de usuario ONVIF", + "description": "Nombre de usuario para la autenticación ONVIF; algunos dispositivos requieren un usuario administrador para ONVIF." + }, + "password": { + "label": "Contraseña ONVIF", + "description": "Contraseña para la autenticación ONVIF." + }, + "ignore_time_mismatch": { + "label": "Ignorar discrepancia horaria", + "description": "Ignora las diferencias de sincronización horaria entre la cámara y el servidor Frigate para la comunicación ONVIF." } }, "objects": { @@ -128,31 +200,138 @@ }, "send_triggers": { "after_significant_updates": { - "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado." + "description": "Envía una solicitud a GenAI tras un número especificado de actualizaciones significativas del objeto rastreado.", + "label": "Activador temprano de GenAI" }, - "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.)." + "description": "Define cuándo se deben enviar los fotogramas a GenAI (al finalizar, después de las actualizaciones, etc.).", + "label": "Activadores de GenAI", + "tracked_object_end": { + "label": "Enviar al finalizar", + "description": "Envía una solicitud a GenAI cuando finaliza el objeto rastreado." + } }, "required_zones": { - "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI." + "description": "Zonas en las que deben ubicarse los objetos para ser elegibles para la generación de descripciones con GenAI.", + "label": "Zonas requeridas" + }, + "prompt": { + "label": "Prompt de descripción", + "description": "Plantilla de prompt predeterminada usada al generar descripciones con GenAI." + }, + "object_prompts": { + "label": "Prompts de objetos", + "description": "Prompts por objeto para personalizar las salidas de GenAI para etiquetas concretas." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de etiquetas de objetos que se enviarán a GenAI de forma predeterminada." + }, + "debug_save_thumbnails": { + "label": "Guardar miniaturas", + "description": "Guarda las miniaturas enviadas a GenAI para depuración y revisión." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Indica si GenAI estaba habilitado en la configuración estática original." } }, "track": { - "description": "Lista de etiquetas de objetos a rastrear para todas las cámaras; puede anularse por cámara." + "description": "Lista de etiquetas de objetos a rastrear para todas las cámaras; puede anularse por cámara.", + "label": "Objetos a rastrear" + }, + "label": "Objetos", + "description": "Valores predeterminados de seguimiento de objetos, incluidas las etiquetas que se rastrean y los filtros por objeto.", + "filters": { + "label": "Filtros de objetos", + "description": "Filtros aplicados a los objetos detectados para reducir falsos positivos (área, relación, confianza).", + "min_area": { + "label": "Área mínima de objeto", + "description": "Área mínima del cuadro delimitador (píxeles o porcentaje) necesaria para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima de objeto", + "description": "Área máxima del cuadro delimitador (píxeles o porcentaje) permitida para este tipo de objeto. Puede ser píxeles (int) o porcentaje (float entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima anchura/altura necesaria para que el cuadro delimitador sea válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima anchura/altura permitida para que el cuadro delimitador sea válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el objeto se considere un positivo verdadero." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para que el objeto se contabilice." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para evitar la detección de objetos en áreas especificadas." + }, + "filters_attribute": { + "label": "Filtros de atributos", + "description": "Filtros aplicados a los atributos detectados para reducir falsos positivos (área, proporción y confianza).", + "min_area": { + "label": "Área mínima del atributo", + "description": "Área mínima del cuadro delimitador (en píxeles o porcentaje) necesaria para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "max_area": { + "label": "Área máxima del atributo", + "description": "Área máxima del cuadro delimitador (en píxeles o porcentaje) permitida para este atributo. Puede expresarse en píxeles (entero) o como porcentaje (valor decimal entre 0.000001 y 0.99)." + }, + "min_ratio": { + "label": "Relación de aspecto mínima", + "description": "Relación mínima entre anchura y altura necesaria para que el cuadro delimitador se considere válido." + }, + "max_ratio": { + "label": "Relación de aspecto máxima", + "description": "Relación máxima entre anchura y altura permitida para que el cuadro delimitador se considere válido." + }, + "threshold": { + "label": "Umbral de confianza", + "description": "Umbral medio de confianza de detección necesario para que el atributo se considere un verdadero positivo." + }, + "min_score": { + "label": "Confianza mínima", + "description": "Confianza mínima de detección en un único fotograma necesaria para asociar este atributo con su objeto principal." + }, + "mask": { + "label": "Máscara de filtro", + "description": "Coordenadas del polígono que definen dónde se aplica este filtro dentro del fotograma." + }, + "raw_mask": { + "label": "Máscara sin procesar" + } } }, "detectors": { "deepstack": { "description": "Detector DeepStack/CodeProject.AI que envía imágenes a una API HTTP remota de DeepStack para la inferencia. No recomendado.", "api_url": { - "description": "La URL de la API de DeepStack." + "description": "La URL de la API de DeepStack.", + "label": "URL de la API de DeepStack" }, "api_timeout": { "label": "Tiempo de espera de la API de DeepStack (en segundos)", "description": "Tiempo máximo permitido para una solicitud a la API de DeepStack." }, "api_key": { - "label": "Clave de API de DeepStack (si es necesaria)" - } + "label": "Clave de API de DeepStack (si es necesaria)", + "description": "Clave API opcional para servicios autenticados de DeepStack." + }, + "label": "DeepStack" }, "type": { "label": "Tipo" @@ -161,96 +340,303 @@ "cpu": { "label": "CPU", "num_threads": { - "label": "Número de hilos para detección" + "label": "Número de hilos para detección", + "description": "Número de hilos usados para inferencia basada en CPU." }, "description": "Detector TFLite de CPU que ejecuta modelos de TensorFlow Lite en la CPU del host sin aceleración por hardware. No recomendado." }, "axengine": { - "label": "Motor AX NPU" + "label": "Motor AX NPU", + "description": "Detector NPU AXERA AX650N/AX8850N que ejecuta archivos .axmodel compilados mediante el runtime AXEngine." }, "teflon_tfl": { - "description": "Detector de delegados Teflon para TFLite, que utiliza la biblioteca de delegados Mesa Teflon para acelerar la inferencia en las GPU compatibles." + "description": "Detector de delegados Teflon para TFLite, que utiliza la biblioteca de delegados Mesa Teflon para acelerar la inferencia en las GPU compatibles.", + "label": "Teflon" }, "synaptics": { - "description": "Detector NPU de Synaptics para modelos en formato .synap, utilizando el Synap SDK en hardware de Synaptics." + "description": "Detector NPU de Synaptics para modelos en formato .synap, utilizando el Synap SDK en hardware de Synaptics.", + "label": "Synaptics" }, "zmq": { - "description": "Detector ZMQ IPC que descarga la inferencia a un proceso externo a través de un punto de conexión IPC de ZeroMQ." + "description": "Detector ZMQ IPC que descarga la inferencia a un proceso externo a través de un punto de conexión IPC de ZeroMQ.", + "label": "IPC de ZMQ", + "endpoint": { + "label": "Endpoint IPC de ZMQ", + "description": "Endpoint ZMQ al que conectarse." + }, + "request_timeout_ms": { + "label": "Tiempo de espera de solicitud ZMQ en milisegundos", + "description": "Tiempo de espera para solicitudes ZMQ en milisegundos." + }, + "linger_ms": { + "label": "Persistencia del socket ZMQ en milisegundos", + "description": "Periodo de persistencia del socket en milisegundos." + } }, "hailo8l": { - "description": "Detector Hailo-8/Hailo-8L que utiliza modelos HEF y el SDK HailoRT para la inferencia en hardware Hailo." + "description": "Detector Hailo-8/Hailo-8L que utiliza modelos HEF y el SDK HailoRT para la inferencia en hardware Hailo.", + "label": "Hailo-8/Hailo-8L", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia Hailo (p. ej., 'PCIe', 'M.2')." + } }, "onnx": { - "description": "Detector ONNX para ejecutar modelos ONNX; utilizará los backends de aceleración disponibles (CUDA/ROCm/OpenVINO) cuando estén disponibles." + "description": "Detector ONNX para ejecutar modelos ONNX; utilizará los backends de aceleración disponibles (CUDA/ROCm/OpenVINO) cuando estén disponibles.", + "label": "ONNX", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia ONNX (p. ej., 'AUTO', 'CPU', 'GPU')." + } }, "description": "Configuración para detectores de objetos (backends de CPU, GPU y ONNX) y cualquier ajuste del modelo específico del detector.", "openvino": { - "description": "Detector OpenVINO para CPU AMD e Intel, GPU Intel y hardware VPU Intel." + "description": "Detector OpenVINO para CPU AMD e Intel, GPU Intel y hardware VPU Intel.", + "label": "OpenVINO", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia OpenVINO (p. ej., 'CPU', 'GPU', 'NPU')." + } }, "tensorrt": { - "description": "Detector TensorRT para dispositivos Nvidia Jetson que utiliza motores TensorRT serializados para una inferencia acelerada." + "description": "Detector TensorRT para dispositivos Nvidia Jetson que utiliza motores TensorRT serializados para una inferencia acelerada.", + "label": "TensorRT", + "device": { + "label": "Índice de dispositivo GPU", + "description": "Índice del dispositivo GPU que se usará." + } }, "degirum": { - "description": "Detector DeGirum para ejecutar modelos a través de la nube de DeGirum o servicios de inferencia local." + "description": "Detector DeGirum para ejecutar modelos a través de la nube de DeGirum o servicios de inferencia local.", + "label": "DeGirum", + "location": { + "label": "Ubicación de inferencia", + "description": "Ubicación del motor de inferencia DeGirum (p. ej., '@cloud', '127.0.0.1')." + }, + "zoo": { + "label": "Repositorio de modelos", + "description": "Ruta o URL al repositorio de modelos de DeGirum." + }, + "token": { + "label": "Token de DeGirum Cloud", + "description": "Token para acceder a DeGirum Cloud." + } }, "rknn": { - "description": "Detector RKNN para NPUs de Rockchip; ejecuta modelos compilados para RKNN en hardware de Rockchip." + "description": "Detector RKNN para NPUs de Rockchip; ejecuta modelos compilados para RKNN en hardware de Rockchip.", + "label": "RKNN", + "num_cores": { + "label": "Número de núcleos NPU que se usarán.", + "description": "Número de núcleos NPU que se usarán (0 para automático)." + } + }, + "model": { + "label": "Configuración de modelo específica del detector", + "description": "Opciones de configuración de modelo específicas del detector (ruta, tamaño de entrada, etc.).", + "path": { + "label": "Ruta del modelo de detector de objetos personalizado", + "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)." + }, + "labelmap_path": { + "label": "Mapa de etiquetas para detector de objetos personalizado", + "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector." + }, + "width": { + "label": "Anchura de entrada del modelo de detección de objetos", + "description": "Anchura del tensor de entrada del modelo en píxeles." + }, + "height": { + "label": "Altura de entrada del modelo de detección de objetos", + "description": "Altura del tensor de entrada del modelo en píxeles." + }, + "labelmap": { + "label": "Personalización del mapa de etiquetas", + "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar." + }, + "attributes_map": { + "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos", + "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensor de entrada del modelo", + "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato de color de píxeles de entrada del modelo", + "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo D de entrada del modelo", + "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')." + }, + "model_type": { + "label": "Tipo de modelo de detección de objetos", + "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización." + } + }, + "model_path": { + "label": "Ruta de modelo específica del detector", + "description": "Ruta del archivo binario del modelo detector si lo requiere el detector elegido." + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Detector EdgeTPU que ejecuta modelos TensorFlow Lite compilados para Coral EdgeTPU mediante el delegado EdgeTPU.", + "device": { + "label": "Tipo de dispositivo", + "description": "Dispositivo que se usará para la inferencia EdgeTPU (p. ej., 'usb', 'pci')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Detector MemryX MX3 que ejecuta modelos DFP compilados en aceleradores MemryX.", + "device": { + "label": "Ruta del dispositivo", + "description": "Dispositivo que se usará para la inferencia MemryX (p. ej., 'PCIe')." + } } }, "database": { "label": "Base de datos", - "description": "Configuración de la base de datos SQLite utilizada por Frigate para almacenar los metadatos de los objetos rastreados y las grabaciones." + "description": "Configuración de la base de datos SQLite utilizada por Frigate para almacenar los metadatos de los objetos rastreados y las grabaciones.", + "path": { + "label": "Ruta de la base de datos", + "description": "Ruta del sistema de archivos donde se almacenará el archivo de base de datos SQLite de Frigate." + } }, "mqtt": { "label": "MQTT", "port": { - "label": "Puerto MQTT" + "label": "Puerto MQTT", + "description": "Puerto del broker MQTT (normalmente 1883 para MQTT sin cifrar)." }, "tls_client_cert": { - "label": "Certificado cliente" + "label": "Certificado cliente", + "description": "Ruta del certificado de cliente para autenticación TLS mutua; no configures usuario/contraseña al usar certificados de cliente." }, "description": "Configuración para conectar y publicar telemetría, instantáneas y detalles de eventos en un broker MQTT.", "topic_prefix": { - "description": "Prefijo del tema MQTT para todos los temas de Frigate; debe ser único si se ejecutan múltiples instancias." + "description": "Prefijo del tema MQTT para todos los temas de Frigate; debe ser único si se ejecutan múltiples instancias.", + "label": "Prefijo de tema" }, "client_id": { - "description": "Identificador de cliente utilizado al conectarse al broker MQTT; debe ser único para cada instancia." + "description": "Identificador de cliente utilizado al conectarse al broker MQTT; debe ser único para cada instancia.", + "label": "ID de cliente" + }, + "enabled": { + "label": "Habilitar MQTT", + "description": "Habilita o deshabilita la integración MQTT para estados, eventos e instantáneas." + }, + "host": { + "label": "Host MQTT", + "description": "Nombre de host o dirección IP del broker MQTT." + }, + "stats_interval": { + "label": "Intervalo de estadísticas", + "description": "Intervalo en segundos para publicar estadísticas del sistema y de las cámaras en MQTT." + }, + "user": { + "label": "Nombre de usuario MQTT", + "description": "Nombre de usuario MQTT opcional; puede proporcionarse mediante variables de entorno o secretos." + }, + "password": { + "label": "Contraseña MQTT", + "description": "Contraseña MQTT opcional; puede proporcionarse mediante variables de entorno o secretos." + }, + "tls_ca_certs": { + "label": "Certificados CA TLS", + "description": "Ruta al certificado CA para conexiones TLS con el broker (para certificados autofirmados)." + }, + "tls_client_key": { + "label": "Clave de cliente", + "description": "Ruta de la clave privada del certificado de cliente." + }, + "tls_insecure": { + "label": "TLS inseguro", + "description": "Permite conexiones TLS inseguras omitiendo la verificación del nombre de host (no recomendado)." + }, + "qos": { + "label": "QoS de MQTT", + "description": "Nivel de calidad de servicio para publicaciones/suscripciones MQTT (0, 1 o 2)." } }, "notifications": { "email": { - "label": "Email de notificacion" - } + "label": "Email de notificacion", + "description": "Dirección de correo electrónico usada para notificaciones push o requerida por ciertos proveedores de notificaciones." + }, + "label": "Notificaciones", + "enabled": { + "label": "Habilitar notificaciones", + "description": "Habilita o deshabilita las notificaciones para todas las cámaras; se puede sobrescribir por cámara." + }, + "cooldown": { + "label": "Periodo de enfriamiento", + "description": "Periodo de enfriamiento (segundos) entre notificaciones para evitar saturar a los destinatarios." + }, + "enabled_in_config": { + "label": "Estado original de notificaciones", + "description": "Indica si las notificaciones estaban habilitadas en la configuración estática original." + }, + "description": "Ajustes para habilitar y controlar las notificaciones de todas las cámaras; se pueden sobrescribir por cámara." }, "networking": { "ipv6": { - "label": "Configuración IPV6" + "label": "Configuración IPV6", + "description": "Ajustes específicos de IPv6 para los servicios de red de Frigate.", + "enabled": { + "label": "Habilitar IPv6", + "description": "Habilita la compatibilidad con IPv6 para los servicios de Frigate (API e interfaz) cuando corresponda." + } }, "listen": { "internal": { - "label": "Puerto interno" + "label": "Puerto interno", + "description": "Puerto de escucha interno de Frigate (predeterminado 5000)." }, "external": { "label": "Puerto externo", "description": "Puerto externo de escucha para Frigate (por defecto 8791)." }, - "description": "Configuración de los puertos de escucha internos y externos. Esto es para usuarios avanzados. Para la mayoría de los casos de uso, se recomienda modificar la sección de puertos de su configuración de Docker Compose." - } + "description": "Configuración de los puertos de escucha internos y externos. Esto es para usuarios avanzados. Para la mayoría de los casos de uso, se recomienda modificar la sección de puertos de su configuración de Docker Compose.", + "label": "Configuración de puertos de escucha" + }, + "label": "Red", + "description": "Ajustes relacionados con la red, como la habilitación de IPv6 para los endpoints de Frigate." }, "proxy": { "label": "Proxy", "separator": { - "label": "Carácter de separación" + "label": "Carácter de separación", + "description": "Carácter usado para separar varios valores proporcionados en las cabeceras del proxy." }, "default_role": { - "description": "Rol predeterminado asignado a los usuarios autenticados por proxy cuando no se aplica ningún mapeo de roles (administrador o espectador)." + "description": "Rol predeterminado asignado a los usuarios autenticados por proxy cuando no se aplica ningún mapeo de roles (administrador o espectador).", + "label": "Rol predeterminado" }, "description": "Configuración para integrar Frigate detrás de un proxy inverso que transmite encabezados de usuario autenticados.", "header_map": { "description": "Mapear los encabezados de proxy entrantes a los campos de usuario y rol de Frigate para la autenticación basada en proxy.", "role": { - "description": "Encabezado que contiene el rol o los grupos del usuario autenticado provenientes del proxy ascendente." + "description": "Encabezado que contiene el rol o los grupos del usuario autenticado provenientes del proxy ascendente.", + "label": "Cabecera de rol" + }, + "label": "Asignación de cabeceras", + "user": { + "label": "Cabecera de usuario", + "description": "Cabecera que contiene el nombre de usuario autenticado proporcionado por el proxy ascendente." + }, + "role_map": { + "label": "Asignación de roles", + "description": "Asigna valores de grupos ascendentes a roles de Frigate (por ejemplo, asignar grupos de administradores al rol de administrador)." } + }, + "logout_url": { + "label": "URL de cierre de sesión", + "description": "URL a la que redirigir a los usuarios al cerrar sesión mediante el proxy." + }, + "auth_secret": { + "label": "Secreto del proxy", + "description": "Secreto opcional que se comprueba con la cabecera X-Proxy-Secret para verificar proxies de confianza." } }, "telemetry": { @@ -261,18 +647,28 @@ "description": "Habilitar la recopilación de estadísticas de la GPU Intel si hay una GPU Intel presente." }, "network_bandwidth": { - "label": "Ancho de banda" + "label": "Ancho de banda", + "description": "Habilita la monitorización del ancho de banda de red por proceso para procesos ffmpeg de cámaras y detectores (requiere capacidades)." }, "amd_gpu_stats": { "label": "Estadísticas GPU Amd", "description": "Habilitar la recopilación de estadísticas de la GPU AMD si hay una GPU AMD presente." }, "intel_gpu_device": { - "description": "Identificador de dispositivo utilizado al tratar las GPU Intel como SR-IOV para corregir las estadísticas de la GPU." - } + "description": "Dirección del bus PCI o ruta del dispositivo DRM (p. ej., /dev/dri/card1) usada para fijar las estadísticas de la GPU Intel a un dispositivo concreto cuando hay varios presentes.", + "label": "Dispositivo GPU Intel" + }, + "label": "Estadísticas del sistema", + "description": "Opciones para habilitar/deshabilitar la recopilación de distintas estadísticas del sistema y de la GPU." }, "version_check": { - "description": "Habilite una verificación saliente para detectar si hay disponible una versión más reciente de Frigate." + "description": "Habilite una verificación saliente para detectar si hay disponible una versión más reciente de Frigate.", + "label": "Comprobación de versión" + }, + "description": "Opciones de telemetría y estadísticas del sistema, incluida la monitorización de GPU y ancho de banda de red.", + "network_interfaces": { + "label": "Interfaces de red", + "description": "Lista de prefijos de nombres de interfaces de red que se monitorizarán para estadísticas de ancho de banda." } }, "ui": { @@ -283,180 +679,957 @@ "unit_system": { "label": "Unidad de sistema", "description": "Sistema de unidades para la visualización (métrico o imperial) utilizado en la interfaz de usuario y en MQTT." + }, + "label": "Interfaz", + "description": "Preferencias de la interfaz de usuario, como zona horaria, formato de fecha/hora y unidades.", + "time_format": { + "label": "Formato de hora", + "description": "Formato de hora que se usará en la interfaz (browser, 12hour o 24hour)." + }, + "date_style": { + "label": "Estilo de fecha", + "description": "Estilo de fecha que se usará en la interfaz (full, long, medium, short)." + }, + "time_style": { + "label": "Estilo de hora", + "description": "Estilo de hora que se usará en la interfaz (full, long, medium, short)." } }, "audio_transcription": { "description": "Configuración para la transcripción de audio en vivo y de voz, utilizada para eventos y subtítulos en tiempo real.", "language": { - "description": "Código de idioma utilizado para la transcripción/traducción (por ejemplo, 'es' para Español). Consulte https://whisper-api.com/docs/languages/ para ver los códigos de idioma compatibles." + "description": "Código de idioma utilizado para la transcripción/traducción (por ejemplo, 'es' para Español). Consulte https://whisper-api.com/docs/languages/ para ver los códigos de idioma compatibles.", + "label": "Idioma de transcripción" }, "enabled": { - "description": "Habilitar o deshabilitar la transcripción automática de audio para todas las cámaras; puede anularse por cámara." + "description": "Habilitar o deshabilitar la transcripción automática de audio para todas las cámaras; puede anularse por cámara.", + "label": "Habilitar transcripción de audio" + }, + "label": "Transcripción de audio", + "live_enabled": { + "label": "Transcripción en directo", + "description": "Activar la transcripción en directo del audio a medida que se recibe." + }, + "device": { + "label": "Dispositivo de transcripción", + "description": "Clave del dispositivo (CPU/GPU) donde ejecutar el modelo de transcripción. Actualmente, solo se admiten GPU NVIDIA CUDA para la transcripción." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo que se usará para la transcripción sin conexión de eventos de audio." } }, "motion": { "skip_motion_threshold": { - "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función." + "description": "Si se establece en un valor entre 0,0 y 1,0, y más de esta fracción de la imagen cambia en un solo fotograma, el detector no devolverá cuadros de movimiento y se recalibrará inmediatamente. Esto puede ahorrar recursos de CPU y reducir los falsos positivos durante tormentas eléctricas, tempestades, etc., aunque podría pasar por alto eventos reales, como el seguimiento automático de un objeto por parte de una cámara PTZ. La disyuntiva está entre descartar unos cuantos megabytes de grabaciones o revisar un par de clips cortos. Deje este parámetro sin establecer (None) para desactivar esta función.", + "label": "Omitir umbral de movimiento" }, "lightning_threshold": { - "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento." + "description": "Umbral para detectar e ignorar breves picos de luz (un valor menor indica mayor sensibilidad; valores entre 0,3 y 1,0). Esto no impide por completo la detección de movimiento; Simplemente provoca que el detector deje de analizar fotogramas adicionales una vez que se supera el umbral. Durante estos eventos aún se realizan grabaciones basadas en el movimiento.", + "label": "Umbral de iluminación" }, "threshold": { - "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255)." + "description": "Umbral de diferencia de píxeles utilizado por el detector de movimiento; los valores más altos reducen la sensibilidad (rango 1-255).", + "label": "Umbral de movimiento" }, "enabled": { - "description": "Habilitar o deshabilitar la detección de movimiento para todas las cámaras; puede anularse para cada cámara individualmente." - } + "description": "Habilitar o deshabilitar la detección de movimiento para todas las cámaras; puede anularse para cada cámara individualmente.", + "label": "Habilitar detección de movimiento" + }, + "label": "Detección de movimiento", + "improve_contrast": { + "label": "Mejorar contraste", + "description": "Aplica una mejora de contraste a los fotogramas antes del análisis de movimiento para ayudar a la detección." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima de contorno en píxeles necesaria para que se cuente un contorno de movimiento." + }, + "delta_alpha": { + "label": "Delta alfa", + "description": "Factor de mezcla alfa usado en la diferencia entre fotogramas para calcular el movimiento." + }, + "frame_alpha": { + "label": "Alfa del fotograma", + "description": "Valor alfa usado al mezclar fotogramas para el preprocesamiento de movimiento." + }, + "frame_height": { + "label": "Altura del fotograma", + "description": "Altura en píxeles a la que escalar los fotogramas al calcular el movimiento." + }, + "mask": { + "label": "Coordenadas de máscara", + "description": "Coordenadas x,y ordenadas que definen el polígono de máscara de movimiento usado para incluir/excluir áreas." + }, + "mqtt_off_delay": { + "label": "Retraso de apagado MQTT", + "description": "Segundos a esperar tras el último movimiento antes de publicar un estado MQTT 'off'." + }, + "enabled_in_config": { + "label": "Estado de movimiento original", + "description": "Indica si la detección de movimiento estaba habilitada en la configuración estática original." + }, + "raw_mask": { + "label": "Máscara sin procesar" + }, + "description": "Ajustes predeterminados de detección de movimiento aplicados a las cámaras salvo que se sobrescriban por cámara." }, "lpr": { "enhancement": { - "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución." + "description": "Nivel de mejora (0-10) que se aplicará a los recortes de matrículas antes del OCR; los valores más altos no siempre mejoran los resultados, y los niveles superiores a 5 podrían funcionar únicamente con matrículas capturadas de noche, por lo que deben utilizarse con precaución.", + "label": "Nivel de mejora" }, "expire_time": { - "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas)." + "description": "Tiempo en segundos tras el cual una matrícula no detectada caduca en el sistema de seguimiento (solo para cámaras LPR dedicadas).", + "label": "Segundos hasta caducar" }, "enabled": { - "description": "Habilitar o deshabilitar el reconocimiento de matrículas para todas las cámaras; puede anularse por cámara." + "description": "Habilitar o deshabilitar el reconocimiento de matrículas para todas las cámaras; puede anularse por cámara.", + "label": "Habilitar LPR" }, "min_plate_length": { - "description": "Número mínimo de caracteres que debe contener una matrícula reconocida para ser considerada válida." + "description": "Número mínimo de caracteres que debe contener una matrícula reconocida para ser considerada válida.", + "label": "Longitud mínima de matrícula" + }, + "label": "Reconocimiento de matrículas", + "description": "Ajustes de reconocimiento de matrículas, incluidos umbrales de detección, formato y matrículas conocidas.", + "min_area": { + "label": "Área mínima de matrícula", + "description": "Área mínima de matrícula (píxeles) necesaria para intentar el reconocimiento." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo usado para detección/reconocimiento de texto. La mayoría de usuarios debería usar 'small'." + }, + "detection_threshold": { + "label": "Umbral de detección", + "description": "Umbral de confianza de detección para empezar a ejecutar OCR en una matrícula sospechosa." + }, + "recognition_threshold": { + "label": "Umbral de reconocimiento", + "description": "Umbral de confianza necesario para adjuntar el texto de matrícula reconocido como subetiqueta." + }, + "format": { + "label": "Regex de formato de matrícula", + "description": "Regex opcional para validar cadenas de matrícula reconocidas frente a un formato esperado." + }, + "match_distance": { + "label": "Distancia de coincidencia", + "description": "Número de diferencias de caracteres permitidas al comparar matrículas detectadas con matrículas conocidas." + }, + "known_plates": { + "label": "Matrículas conocidas", + "description": "Lista de matrículas o regexes que se rastrearán especialmente o sobre las que se alertará." + }, + "debug_save_plates": { + "label": "Guardar matrículas de depuración", + "description": "Guarda imágenes recortadas de matrículas para depurar el rendimiento de LPR." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" + }, + "replace_rules": { + "label": "Reglas de sustitución", + "description": "Reglas de sustitución regex usadas para normalizar cadenas de matrícula detectadas antes de compararlas.", + "pattern": { + "label": "Patrón regex" + }, + "replacement": { + "label": "Cadena de sustitución" + } } }, "detect": { "fps": { - "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez)." + "description": "Fotogramas por segundo deseados para ejecutar la detección; los valores más bajos reducen el uso de la CPU (el valor recomendado es 5; establezca un valor superior —como máximo de 10— únicamente si realiza el seguimiento de objetos que se mueven con extrema rapidez).", + "label": "FPS de detección" }, "min_initialized": { - "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2." + "description": "Número de detecciones consecutivas requeridas antes de crear un objeto rastreado. Auméntelo para reducir las inicializaciones falsas. El valor predeterminado es los FPS divididos por 2.", + "label": "Fotogramas mínimos de inicialización" }, "height": { - "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión." + "description": "Altura (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Altura de detección" }, "width": { - "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión." + "description": "Ancho (en píxeles) de los fotogramas utilizados para la transmisión de detección; déjelo vacío para utilizar la resolución nativa de la transmisión.", + "label": "Anchura de detección" }, "stationary": { - "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo." + "description": "Configuración para detectar y gestionar objetos que permanecen inmóviles durante un periodo de tiempo.", + "label": "Configuración de objetos estacionarios", + "interval": { + "label": "Intervalo estacionario", + "description": "Frecuencia (en fotogramas) con la que se ejecuta una comprobación de detección para confirmar un objeto estacionario." + }, + "threshold": { + "label": "Umbral estacionario", + "description": "Número de fotogramas sin cambio de posición necesarios para marcar un objeto como estacionario." + }, + "max_frames": { + "label": "Fotogramas máximos", + "description": "Limita durante cuánto tiempo se rastrean los objetos estacionarios antes de descartarlos.", + "default": { + "label": "Fotogramas máximos predeterminados", + "description": "Número máximo predeterminado de fotogramas para rastrear un objeto estacionario antes de detenerse." + }, + "objects": { + "label": "Fotogramas máximos por objeto", + "description": "Sobrescrituras por objeto para el número máximo de fotogramas en los que rastrear objetos estacionarios." + } + }, + "classifier": { + "label": "Habilitar clasificador visual", + "description": "Usa un clasificador visual para detectar objetos realmente estacionarios incluso cuando los cuadros delimitadores oscilan." + } }, "enabled": { - "description": "Habilitar o deshabilitar la detección de objetos para todas las cámaras; puede anularse para cada cámara individualmente." + "description": "Habilitar o deshabilitar la detección de objetos para todas las cámaras; puede anularse para cada cámara individualmente.", + "label": "Habilitar detección de objetos" + }, + "label": "Detección de objetos", + "description": "Ajustes del rol de detección/detect usado para ejecutar la detección de objetos e inicializar los rastreadores.", + "max_disappeared": { + "label": "Fotogramas máximos desaparecido", + "description": "Número de fotogramas sin detección antes de que un objeto rastreado se considere desaparecido." + }, + "annotation_offset": { + "label": "Desplazamiento de anotaciones", + "description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo." } }, "record": { "motion": { - "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones." + "description": "Número de días para conservar las grabaciones activadas por movimiento, independientemente de los objetos rastreados. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención de movimiento", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } }, "continuous": { - "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones." + "description": "Número de días para conservar las grabaciones, independientemente de los objetos rastreados o del movimiento. Establézcalo en 0 si solo desea conservar las grabaciones de alertas y detecciones.", + "label": "Retención continua", + "days": { + "label": "Días de retención", + "description": "Días durante los que conservar las grabaciones." + } }, "detections": { "pre_capture": { - "description": "Número de segundos antes del evento de detección que se incluirán en la grabación." + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" }, "post_capture": { - "description": "Número de segundos después del evento de detección que se incluirán en la grabación." + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de detección", + "description": "Ajustes de retención de grabaciones para eventos de detección, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } } }, "alerts": { "pre_capture": { - "description": "Número de segundos antes del evento de detección que se incluirán en la grabación." + "description": "Número de segundos antes del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura previa" }, "post_capture": { - "description": "Número de segundos después del evento de detección que se incluirán en la grabación." + "description": "Número de segundos después del evento de detección que se incluirán en la grabación.", + "label": "Segundos de captura posterior" + }, + "label": "Retención de alertas", + "description": "Ajustes de retención de grabaciones para eventos de alerta, incluidas las duraciones de captura previa/posterior.", + "retain": { + "label": "Retención de eventos", + "description": "Ajustes de retención para grabaciones de eventos de detección.", + "days": { + "label": "Días de retención", + "description": "Número de días durante los que conservar grabaciones de eventos de detección." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + } } - } + }, + "label": "Grabación", + "enabled": { + "label": "Habilitar grabación", + "description": "Habilita o deshabilita la grabación para todas las cámaras; se puede sobrescribir por cámara." + }, + "expire_interval": { + "label": "Intervalo de limpieza de grabaciones", + "description": "Minutos entre pasadas de limpieza que eliminan segmentos de grabación caducados." + }, + "export": { + "label": "Configuración de exportación", + "description": "Ajustes usados al exportar grabaciones, como timelapse y aceleración por hardware.", + "hwaccel_args": { + "label": "Argumentos hwaccel de exportación", + "description": "Argumentos de aceleración por hardware que se usarán en operaciones de exportación/transcodificación." + }, + "max_concurrent": { + "label": "Exportaciones simultáneas máximas", + "description": "Número máximo de trabajos de exportación que se procesarán al mismo tiempo." + } + }, + "preview": { + "label": "Configuración de vista previa", + "description": "Ajustes que controlan la calidad de las vistas previas de grabaciones mostradas en la interfaz.", + "quality": { + "label": "Calidad de vista previa", + "description": "Nivel de calidad de vista previa (very_low, low, medium, high, very_high)." + } + }, + "enabled_in_config": { + "label": "Estado de grabación original", + "description": "Indica si la grabación estaba habilitada en la configuración estática original." + }, + "description": "Ajustes de grabación y retención aplicados a las cámaras salvo que se sobrescriban por cámara." }, "camera_ui": { "dashboard": { - "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz." + "description": "Alterna si esta cámara es visible en toda la interfaz de usuario de Frigate. Desactivar esta opción requerirá editar manualmente la configuración para volver a visualizar esta cámara en la interfaz.", + "label": "Mostrar en la interfaz" + }, + "label": "Interfaz de cámara", + "description": "Orden de visualización y visibilidad de esta cámara en la interfaz. El orden afecta al panel predeterminado. Para un control más granular, usa grupos de cámaras.", + "order": { + "label": "Orden en la interfaz", + "description": "Orden numérico usado para ordenar la cámara en la interfaz (panel predeterminado y listas); los números más altos aparecen más tarde." } }, "live": { "description": "Configuración para controlar la resolución y la calidad de la transmisión en vivo de jsmpeg. Esto no afecta a las cámaras retransmitidas que utilizan go2rtc para la visualización en vivo.", "height": { - "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección." + "description": "Altura (en píxeles) para renderizar la transmisión en vivo de jsmpeg en la interfaz web; debe ser <= a la altura de la transmisión de detección.", + "label": "Altura en directo" + }, + "label": "Reproducción en directo", + "streams": { + "label": "Nombres de flujos en directo", + "description": "Asignación de nombres de flujos configurados a nombres de restream/go2rtc usados para la reproducción en directo." + }, + "quality": { + "label": "Calidad en directo", + "description": "Calidad de codificación para el flujo jsmpeg (1 la más alta, 31 la más baja)." } }, "semantic_search": { "model": { - "description": "El modelo de embeddings a utilizar para la búsqueda semántica (por ejemplo, 'jinav1'), o el nombre de un proveedor de GenAI con el rol de embeddings." + "description": "El modelo de embeddings a utilizar para la búsqueda semántica (por ejemplo, 'jinav1'), o el nombre de un proveedor de GenAI con el rol de embeddings.", + "label": "Modelo de búsqueda semántica o nombre del proveedor GenAI" + }, + "label": "Búsqueda semántica", + "triggers": { + "label": "Activadores", + "description": "Acciones y criterios de coincidencia para activadores de búsqueda semántica específicos de la cámara.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre descriptivo opcional mostrado en la interfaz para este activador." + }, + "enabled": { + "label": "Habilitar este activador", + "description": "Habilita o deshabilita este activador de búsqueda semántica." + }, + "type": { + "label": "Tipo de activador", + "description": "Tipo de activador: 'thumbnail' (coincidir con imagen) o 'description' (coincidir con texto)." + }, + "data": { + "label": "Contenido del activador", + "description": "Frase de texto o ID de miniatura que se comparará con objetos rastreados." + }, + "threshold": { + "label": "Umbral del activador", + "description": "Puntuación mínima de similitud (0-1) necesaria para activar este activador." + }, + "actions": { + "label": "Acciones del activador", + "description": "Lista de acciones que se ejecutarán cuando el activador coincida (notification, sub_label, attribute)." + } + }, + "description": "Ajustes de la búsqueda semántica, que crea y consulta embeddings de objetos para encontrar elementos similares.", + "enabled": { + "label": "Habilitar búsqueda semántica", + "description": "Habilita o deshabilita la función de búsqueda semántica." + }, + "reindex": { + "label": "Reindexar al iniciar", + "description": "Activa una reindexación completa de los objetos rastreados históricos en la base de datos de embeddings." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Selecciona el tamaño del modelo; 'small' se ejecuta en CPU y 'large' normalmente requiere GPU." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" } }, "review": { "alerts": { "required_zones": { - "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona." + "description": "Zonas en las que debe entrar un objeto para ser considerado una alerta; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" }, "labels": { - "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person)." + "description": "Lista de etiquetas de objetos que califican como alertas (por ejemplo: car, person).", + "label": "Etiquetas de alerta" + }, + "label": "Configuración de alertas", + "description": "Ajustes sobre qué objetos rastreados generan alertas y cómo se conservan las alertas.", + "enabled": { + "label": "Habilitar alertas", + "description": "Habilita o deshabilita la generación de alertas para todas las cámaras; se puede sobrescribir por cámara." + }, + "enabled_in_config": { + "label": "Estado original de alertas", + "description": "Rastrea si las alertas estaban habilitadas originalmente en la configuración estática." + }, + "cutoff_time": { + "label": "Tiempo de corte de alertas", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de alerta antes de cortar una alerta." } }, "detections": { "required_zones": { - "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona." + "description": "Zonas en las que debe entrar un objeto para ser considerado detectado; dejar vacío para permitir cualquier zona.", + "label": "Zonas requeridas" }, - "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones." + "description": "Configuración para determinar qué objetos rastreados generan detecciones (no alertas) y cómo se retienen dichas detecciones.", + "label": "Configuración de detecciones", + "enabled": { + "label": "Habilitar detecciones", + "description": "Habilita o deshabilita los eventos de detección para todas las cámaras; se puede sobrescribir por cámara." + }, + "labels": { + "label": "Etiquetas de detección", + "description": "Lista de etiquetas de objetos que cuentan como eventos de detección." + }, + "cutoff_time": { + "label": "Tiempo de corte de detecciones", + "description": "Segundos que se esperarán tras dejar de haber actividad causante de detección antes de cortar una detección." + }, + "enabled_in_config": { + "label": "Estado original de detecciones", + "description": "Rastrea si las detecciones estaban habilitadas originalmente en la configuración estática." + } }, "genai": { "image_source": { - "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens." + "description": "Fuente de las imágenes enviadas a GenAI ('preview' o 'recordings'); La opción 'recordings' utiliza fotogramas de mayor calidad, pero requiere más tokens.", + "label": "Origen de imagen de revisión" }, "additional_concerns": { - "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara." + "description": "Una lista de preocupaciones o notas adicionales que GenAI debería tener en cuenta al evaluar la actividad en esta cámara.", + "label": "Consideraciones adicionales" }, "activity_context_prompt": { - "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI." + "description": "Instrucción personalizada que describe qué constituye y qué no una actividad sospechosa, con el fin de proporcionar contexto para los resúmenes generados por GenAI.", + "label": "Prompt de contexto de actividad" }, "description": "Controla el uso de IA generativa (GenAI) para la elaboración de descripciones y resúmenes de elementos de revisión.", "debug_save_thumbnails": { - "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión." + "description": "Guarde las miniaturas que se envían al proveedor de GenAI para su depuración y revisión.", + "label": "Guardar miniaturas" + }, + "label": "Configuración de GenAI", + "enabled": { + "label": "Habilitar descripciones de GenAI", + "description": "Habilita o deshabilita las descripciones y resúmenes generados por GenAI para los elementos de revisión." + }, + "alerts": { + "label": "Habilitar GenAI para alertas", + "description": "Usa GenAI para generar descripciones de elementos de alerta." + }, + "detections": { + "label": "Habilitar GenAI para detecciones", + "description": "Usa GenAI para generar descripciones de elementos de detección." + }, + "enabled_in_config": { + "label": "Estado original de GenAI", + "description": "Rastrea si la revisión de GenAI estaba habilitada originalmente en la configuración estática." + }, + "preferred_language": { + "label": "Idioma preferido", + "description": "Idioma preferido que se solicitará al proveedor de GenAI para las respuestas generadas." } - } + }, + "label": "Revisión", + "description": "Ajustes que controlan alertas, detecciones y resúmenes de revisión de GenAI usados por la interfaz y el almacenamiento." }, "birdseye": { "description": "Configuración para la vista compuesta Birdseye, que combina las transmisiones de múltiples cámaras en una sola vista.", "restream": { - "description": "Retransmita la salida de video de Birdseye como una transmisión en vivo RTSP; al habilitar esta opción, Birdseye se mantendrá en ejecución de forma continua." + "description": "Retransmita la salida de video de Birdseye como una transmisión en vivo RTSP; al habilitar esta opción, Birdseye se mantendrá en ejecución de forma continua.", + "label": "Retransmisión RTSP" }, "layout": { "max_cameras": { - "description": "Número máximo de cámaras a mostrar simultáneamente en Birdseye; muestra las cámaras más recientes." + "description": "Número máximo de cámaras a mostrar simultáneamente en Birdseye; muestra las cámaras más recientes.", + "label": "Cámaras máximas" + }, + "label": "Diseño", + "description": "Opciones de diseño para la composición de Birdseye.", + "scaling_factor": { + "label": "Factor de escala", + "description": "Factor de escala usado por el calculador de diseño (rango de 1.0 a 5.0)." } + }, + "label": "Vista general", + "enabled": { + "label": "Habilitar Birdseye", + "description": "Habilita o deshabilita la función de vista Birdseye." + }, + "mode": { + "label": "Modo de seguimiento", + "description": "Modo para incluir cámaras en Birdseye: 'objects', 'motion' o 'continuous'." + }, + "order": { + "label": "Posición", + "description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye." + }, + "width": { + "label": "Anchura", + "description": "Anchura de salida (píxeles) del fotograma compuesto de Birdseye." + }, + "height": { + "label": "Altura", + "description": "Altura de salida (píxeles) del fotograma compuesto de Birdseye." + }, + "quality": { + "label": "Calidad de codificación", + "description": "Calidad de codificación para el flujo mpeg1 de Birdseye (1 la calidad más alta, 31 la más baja)." + }, + "inactivity_threshold": { + "label": "Umbral de inactividad", + "description": "Segundos de inactividad tras los cuales una cámara dejará de mostrarse en Birdseye." + }, + "idle_heartbeat_fps": { + "label": "FPS de latido en reposo", + "description": "Fotogramas por segundo para reenviar el último fotograma compuesto de Birdseye en reposo; establécelo en 0 para deshabilitarlo." } }, "ffmpeg": { "retry_interval": { - "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10." + "description": "Segundos de espera antes de intentar reconectar la transmisión de una cámara tras un fallo. El valor predeterminado es 10.", + "label": "Tiempo de reintento de FFmpeg" }, "path": { - "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\")." + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\").", + "label": "Ruta de FFmpeg" }, "output_args": { - "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación." + "description": "Argumentos de salida predeterminados utilizados para diferentes roles de FFmpeg, tales como detección y grabación.", + "label": "Argumentos de salida", + "detect": { + "label": "Argumentos de salida de detección", + "description": "Argumentos de salida predeterminados para los flujos con rol de detección." + }, + "record": { + "label": "Argumentos de salida de grabación", + "description": "Argumentos de salida predeterminados para los flujos con rol de grabación." + } }, - "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol." + "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.", + "label": "FFmpeg", + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales pasados a los procesos de FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para FFmpeg. Se recomiendan preajustes específicos del proveedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados a los flujos de entrada de FFmpeg." + }, + "apple_compatibility": { + "label": "Compatibilidad con Apple", + "description": "Habilita el etiquetado HEVC para mejorar la compatibilidad con reproductores de Apple al grabar H.265." + }, + "gpu": { + "label": "Índice de GPU", + "description": "Índice de GPU predeterminado usado para la aceleración por hardware si está disponible." + }, + "inputs": { + "label": "Entradas de cámara", + "description": "Lista de definiciones de flujos de entrada (rutas y roles) para esta cámara.", + "path": { + "label": "Ruta de entrada", + "description": "URL o ruta del flujo de entrada de la cámara." + }, + "roles": { + "label": "Roles de entrada", + "description": "Roles para este flujo de entrada." + }, + "global_args": { + "label": "Argumentos globales de FFmpeg", + "description": "Argumentos globales de FFmpeg para este flujo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleración por hardware", + "description": "Argumentos de aceleración por hardware para este flujo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este flujo." + } + } }, "go2rtc": { - "description": "Configuración del servicio integrado de retransmisión go2rtc, utilizado para el relevo y la traducción de transmisiones en vivo." + "description": "Configuración del servicio integrado de retransmisión go2rtc, utilizado para el relevo y la traducción de transmisiones en vivo.", + "label": "go2rtc" }, "genai": { "description": "Configuración para los proveedores integrados de IA generativa (GenAI) utilizados para generar descripciones de objetos y resúmenes de reseñas.", "api_key": { - "description": "Clave de API requerida por algunos proveedores (también puede configurarse mediante variables de entorno)." + "description": "Clave de API requerida por algunos proveedores (también puede configurarse mediante variables de entorno).", + "label": "Clave API" }, "base_url": { - "description": "URL base para proveedores autoalojados o compatibles (por ejemplo, una instancia de Ollama)." + "description": "URL base para proveedores autoalojados o compatibles (por ejemplo, una instancia de Ollama).", + "label": "URL base" }, "model": { - "description": "El modelo del proveedor que se utilizará para generar descripciones o resúmenes." + "description": "El modelo del proveedor que se utilizará para generar descripciones o resúmenes.", + "label": "Modelo" + }, + "label": "Configuración de IA generativa", + "provider": { + "label": "Proveedor", + "description": "Proveedor de GenAI que se usará (por ejemplo: ollama, gemini, openai)." + }, + "roles": { + "label": "Roles", + "description": "Roles de GenAI (chat, descriptions, embeddings); un proveedor por rol." + }, + "provider_options": { + "label": "Opciones del proveedor", + "description": "Opciones adicionales específicas del proveedor que se pasarán al cliente GenAI." + }, + "runtime_options": { + "label": "Opciones de ejecución", + "description": "Opciones de ejecución pasadas al proveedor para cada llamada de inferencia." } }, "face_recognition": { - "description": "Configuración para la detección y el reconocimiento facial en todas las cámaras; puede anularse por cámara." + "description": "Configuración para la detección y el reconocimiento facial en todas las cámaras; puede anularse por cámara.", + "label": "Reconocimiento facial", + "enabled": { + "label": "Habilitar reconocimiento facial", + "description": "Habilita o deshabilita el reconocimiento facial para todas las cámaras; se puede sobrescribir por cámara." + }, + "min_area": { + "label": "Área mínima de rostro", + "description": "Área mínima (píxeles) del cuadro de un rostro detectado necesaria para intentar el reconocimiento." + }, + "model_size": { + "label": "Tamaño del modelo", + "description": "Tamaño del modelo que se usará para embeddings faciales (small/large); el más grande puede requerir GPU." + }, + "unknown_score": { + "label": "Umbral de puntuación desconocida", + "description": "Umbral de distancia por debajo del cual un rostro se considera una posible coincidencia (más alto = más estricto)." + }, + "detection_threshold": { + "label": "Umbral de detección", + "description": "Confianza mínima de detección necesaria para considerar válida una detección de rostro." + }, + "recognition_threshold": { + "label": "Umbral de reconocimiento", + "description": "Umbral de distancia de embedding facial para considerar que dos rostros coinciden." + }, + "min_faces": { + "label": "Rostros mínimos", + "description": "Número mínimo de reconocimientos faciales necesarios antes de aplicar una subetiqueta reconocida a una persona." + }, + "save_attempts": { + "label": "Guardar intentos", + "description": "Número de intentos de reconocimiento facial que se conservarán para la interfaz de reconocimientos recientes." + }, + "blur_confidence_filter": { + "label": "Filtro de confianza por desenfoque", + "description": "Ajusta las puntuaciones de confianza según el desenfoque de la imagen para reducir falsos positivos en rostros de baja calidad." + }, + "device": { + "label": "Dispositivo", + "description": "Esto es una sobrescritura para apuntar a un dispositivo concreto. Consulta https://onnxruntime.ai/docs/execution-providers/ para obtener más información" + } }, "camera_mqtt": { "required_zones": { - "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT." + "description": "Zonas en las que debe entrar un objeto para que se publique una imagen MQTT.", + "label": "Zonas requeridas" + }, + "label": "MQTT", + "description": "Ajustes de publicación de imágenes MQTT.", + "enabled": { + "label": "Enviar imagen", + "description": "Habilita la publicación de instantáneas de objetos en temas MQTT para esta cámara." + }, + "timestamp": { + "label": "Añadir marca de tiempo", + "description": "Superpone una marca de tiempo en las imágenes publicadas en MQTT." + }, + "bounding_box": { + "label": "Añadir cuadro delimitador", + "description": "Dibuja cuadros delimitadores en las imágenes publicadas mediante MQTT." + }, + "crop": { + "label": "Recortar imagen", + "description": "Recorta las imágenes publicadas en MQTT al cuadro delimitador del objeto detectado." + }, + "height": { + "label": "Altura de imagen", + "description": "Altura (píxeles) a la que redimensionar las imágenes publicadas mediante MQTT." + }, + "quality": { + "label": "Calidad JPEG", + "description": "Calidad JPEG de las imágenes publicadas en MQTT (0-100)." } + }, + "snapshots": { + "label": "Instantáneas", + "enabled": { + "label": "Habilitar instantáneas", + "description": "Habilita o deshabilita el guardado de instantáneas para todas las cámaras; se puede sobrescribir por cámara." + }, + "timestamp": { + "label": "Superposición de marca de tiempo", + "description": "Superpone una marca de tiempo en las instantáneas de la API." + }, + "bounding_box": { + "label": "Superposición de cuadro delimitador", + "description": "Dibuja cuadros delimitadores para los objetos rastreados en las instantáneas de la API." + }, + "crop": { + "label": "Recortar instantánea", + "description": "Recorta las instantáneas de la API al cuadro delimitador del objeto detectado." + }, + "required_zones": { + "label": "Zonas requeridas", + "description": "Zonas en las que debe entrar un objeto para que se guarde una instantánea." + }, + "height": { + "label": "Altura de instantánea", + "description": "Altura (píxeles) a la que redimensionar las instantáneas de la API; déjalo vacío para conservar el tamaño original." + }, + "retain": { + "label": "Retención de instantáneas", + "description": "Ajustes de retención de instantáneas, incluidos días predeterminados y sobrescrituras por objeto.", + "default": { + "label": "Retención predeterminada", + "description": "Número predeterminado de días durante los que conservar instantáneas." + }, + "mode": { + "label": "Modo de retención", + "description": "Modo de retención: all (guarda todos los segmentos), motion (guarda segmentos con movimiento) o active_objects (guarda segmentos con objetos activos)." + }, + "objects": { + "label": "Retención por objeto", + "description": "Sobrescrituras por objeto para los días de retención de instantáneas." + } + }, + "quality": { + "label": "Calidad de instantánea", + "description": "Calidad de codificación de las instantáneas guardadas (0-100)." + }, + "description": "Ajustes para instantáneas generadas por la API de objetos rastreados en todas las cámaras; se pueden sobrescribir por cámara." + }, + "timestamp_style": { + "label": "Estilo de marca de tiempo", + "position": { + "label": "Posición de marca de tiempo", + "description": "Posición de la marca de tiempo en la imagen (tl/tr/bl/br)." + }, + "format": { + "label": "Formato de marca de tiempo", + "description": "Cadena de formato de fecha y hora usada para las marcas de tiempo (códigos de formato datetime de Python)." + }, + "color": { + "label": "Color de marca de tiempo", + "description": "Valores de color RGB para el texto de la marca de tiempo (todos los valores 0-255).", + "red": { + "label": "Rojo", + "description": "Componente rojo (0-255) para el color de la marca de tiempo." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) para el color de la marca de tiempo." + }, + "blue": { + "label": "Azul", + "description": "Componente azul (0-255) para el color de la marca de tiempo." + } + }, + "thickness": { + "label": "Grosor de marca de tiempo", + "description": "Grosor de línea del texto de la marca de tiempo." + }, + "effect": { + "label": "Efecto de marca de tiempo", + "description": "Efecto visual para el texto de la marca de tiempo (none, solid, shadow)." + }, + "description": "Opciones de estilo para marcas de tiempo integradas aplicadas a la vista de depuración y a las instantáneas." + }, + "profiles": { + "label": "Perfiles", + "description": "Definiciones de perfiles con nombre y nombres descriptivos. Los perfiles de cámara deben hacer referencia a nombres definidos aquí.", + "friendly_name": { + "label": "Nombre descriptivo", + "description": "Nombre mostrado para este perfil en la interfaz." + } + }, + "tls": { + "label": "TLS", + "description": "Ajustes TLS para los endpoints web de Frigate (puerto 8971).", + "enabled": { + "label": "Habilitar TLS", + "description": "Habilita TLS para la interfaz web y la API de Frigate en el puerto TLS configurado." + } + }, + "model": { + "label": "Modelo de detección", + "description": "Ajustes para configurar un modelo de detección de objetos personalizado y su forma de entrada.", + "path": { + "label": "Ruta del modelo de detector de objetos personalizado", + "description": "Ruta a un archivo de modelo de detección personalizado (o plus:// para modelos de Frigate+)." + }, + "labelmap_path": { + "label": "Mapa de etiquetas para detector de objetos personalizado", + "description": "Ruta a un archivo labelmap que asigna clases numéricas a etiquetas de texto para el detector." + }, + "width": { + "label": "Anchura de entrada del modelo de detección de objetos", + "description": "Anchura del tensor de entrada del modelo en píxeles." + }, + "height": { + "label": "Altura de entrada del modelo de detección de objetos", + "description": "Altura del tensor de entrada del modelo en píxeles." + }, + "labelmap": { + "label": "Personalización del mapa de etiquetas", + "description": "Sobrescrituras o entradas de reasignación que se fusionarán con el mapa de etiquetas estándar." + }, + "attributes_map": { + "label": "Mapa de etiquetas de objetos a sus etiquetas de atributos", + "description": "Asignación de etiquetas de objetos a etiquetas de atributos usada para adjuntar metadatos (por ejemplo, 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensor de entrada del modelo", + "description": "Formato de tensor esperado por el modelo: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato de color de píxeles de entrada del modelo", + "description": "Espacio de color de píxeles esperado por el modelo: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo D de entrada del modelo", + "description": "Tipo de datos del tensor de entrada del modelo (por ejemplo, 'float32')." + }, + "model_type": { + "label": "Tipo de modelo de detección de objetos", + "description": "Tipo de arquitectura del modelo detector (ssd, yolox, yolonas) usado por algunos detectores para optimización." + } + }, + "classification": { + "label": "Clasificación de objetos", + "description": "Ajustes de los modelos de clasificación usados para refinar etiquetas de objetos o clasificación de estado.", + "bird": { + "label": "Configuración de clasificación de aves", + "description": "Ajustes específicos de los modelos de clasificación de aves.", + "enabled": { + "label": "Clasificación de aves", + "description": "Habilita o deshabilita la clasificación de aves." + }, + "threshold": { + "label": "Puntuación mínima", + "description": "Puntuación mínima de clasificación necesaria para aceptar una clasificación de ave." + } + }, + "custom": { + "label": "Modelos de clasificación personalizados", + "description": "Configuración de modelos de clasificación personalizados usados para objetos o detección de estado.", + "enabled": { + "label": "Habilitar modelo", + "description": "Habilita o deshabilita el modelo de clasificación personalizado." + }, + "name": { + "label": "Nombre del modelo", + "description": "Identificador del modelo de clasificación personalizado que se usará." + }, + "threshold": { + "label": "Umbral de puntuación", + "description": "Umbral de puntuación usado para cambiar el estado de clasificación." + }, + "save_attempts": { + "label": "Guardar intentos", + "description": "Cuántos intentos de clasificación se guardarán para la interfaz de clasificaciones recientes." + }, + "object_config": { + "objects": { + "label": "Clasificar objetos", + "description": "Lista de tipos de objetos sobre los que ejecutar la clasificación de objetos." + }, + "classification_type": { + "label": "Tipo de clasificación", + "description": "Tipo de clasificación aplicado: 'sub_label' (añade sub_label) u otros tipos compatibles." + } + }, + "state_config": { + "cameras": { + "label": "Cámaras de clasificación", + "description": "Recorte y ajustes por cámara para ejecutar la clasificación de estado.", + "crop": { + "label": "Recorte de clasificación", + "description": "Coordenadas de recorte que se usarán para ejecutar la clasificación en esta cámara." + } + }, + "motion": { + "label": "Ejecutar con movimiento", + "description": "Si es true, ejecuta la clasificación cuando se detecte movimiento dentro del recorte especificado." + }, + "interval": { + "label": "Intervalo de clasificación", + "description": "Intervalo (segundos) entre ejecuciones periódicas de clasificación para la clasificación de estado." + } + } + } + }, + "camera_groups": { + "label": "Grupos de cámaras", + "description": "Configuración de grupos de cámaras con nombre usados para organizar cámaras en la interfaz.", + "cameras": { + "label": "Lista de cámaras", + "description": "Array de nombres de cámaras incluidos en este grupo." + }, + "icon": { + "label": "Icono de grupo", + "description": "Icono usado para representar el grupo de cámaras en la interfaz." + }, + "order": { + "label": "Orden de clasificación", + "description": "Orden numérico usado para ordenar grupos de cámaras en la interfaz; los números más altos aparecen más tarde." + } + }, + "active_profile": { + "label": "Perfil activo", + "description": "Nombre del perfil activo actualmente. Solo en tiempo de ejecución, no se conserva en YAML." } } diff --git a/web/public/locales/es/objects.json b/web/public/locales/es/objects.json index fe4d16915e..94adda5cb9 100644 --- a/web/public/locales/es/objects.json +++ b/web/public/locales/es/objects.json @@ -116,5 +116,15 @@ "animal": "Animal", "postnord": "PostNord", "usps": "USPS", - "gls": "GLS" + "gls": "GLS", + "canada_post": "Canada Post", + "royal_mail": "Royal Mail", + "school_bus": "Autobús escolar", + "skunk": "Mofeta", + "kangaroo": "Canguro", + "baby": "Bebé", + "baby_stroller": "Cochecito de bebé", + "rickshaw": "Rickshaw", + "Rodent": "Roedor", + "rodent": "Roedor" } diff --git a/web/public/locales/es/views/chat.json b/web/public/locales/es/views/chat.json index 0967ef424b..876ee2707d 100644 --- a/web/public/locales/es/views/chat.json +++ b/web/public/locales/es/views/chat.json @@ -1 +1,69 @@ -{} +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Tu asistente de IA para la gestión de cámaras y análisis", + "placeholder": "Pregunta cualquier cosa...", + "error": "Algo salió mal. Por favor, inténtalo de nuevo.", + "processing": "Procesando...", + "toolsUsed": "Usado: {{tools}}", + "showTools": "Mostrar herramientas ({{count}})", + "hideTools": "Ocultar herramientas", + "call": "Llamar", + "result": "Resultado", + "arguments": "Argumentos:", + "response": "Respuesta:", + "attachment_chip_label": "{{label}} en {{camera}}", + "attachment_chip_remove": "Eliminar adjunto", + "open_in_explore": "Abrir en Explorar", + "attach_event_aria": "Adjuntar evento {{eventId}}", + "attachment_picker_paste_label": "O pega el ID del evento", + "attachment_picker_attach": "Adjuntar", + "attachment_picker_placeholder": "Adjuntar un evento", + "quick_reply_find_similar": "Buscar avistamientos similares", + "quick_reply_tell_me_more": "Cuéntame más sobre esto", + "quick_reply_when_else": "¿Cuándo más se vio?", + "quick_reply_find_similar_text": "Buscar avistamientos similares a este.", + "quick_reply_tell_me_more_text": "Cuéntame más sobre este.", + "quick_reply_when_else_text": "¿Cuándo más se vio esto?", + "anchor": "Referencia", + "similarity_score": "Similitud", + "no_similar_objects_found": "No se encontraron objetos similares.", + "semantic_search_required": "La búsqueda semántica debe estar activada para encontrar objetos similares.", + "send": "Enviar", + "suggested_requests": "Prueba preguntando:", + "starting_requests": { + "show_recent_events": "Mostrar eventos recientes", + "show_camera_status": "Mostrar estado de la cámara", + "recap": "¿Qué ha pasado mientras estaba fuera?", + "watch_camera": "Vigilar una cámara en busca de actividad" + }, + "starting_requests_prompts": { + "show_recent_events": "Muéstrame los eventos recientes de la última hora", + "show_camera_status": "¿Cuál es el estado actual de mis cámaras?", + "recap": "¿Qué ha pasado mientras estaba fuera?", + "watch_camera": "Vigila la puerta principal y avísame si aparece alguien" + }, + "new_chat": "Nuevo chat", + "settings": { + "title": "Ajustes del chat", + "show_stats": { + "title": "Mostrar estadísticas", + "desc": "Mostrar la velocidad de generación y el tamaño del contexto en las respuestas del chat.", + "while_generating": "Durante la generación", + "always": "Siempre" + }, + "auto_scroll": { + "title": "Desplazamiento automático", + "desc": "Seguir los mensajes nuevos a medida que llegan." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + }, + "reasoning": { + "active": "Razonando…", + "show": "Mostrar razonamiento", + "hide": "Ocultar razonamiento" + } +} diff --git a/web/public/locales/es/views/classificationModel.json b/web/public/locales/es/views/classificationModel.json index 0f5ec539b9..e1d1449e8e 100644 --- a/web/public/locales/es/views/classificationModel.json +++ b/web/public/locales/es/views/classificationModel.json @@ -146,7 +146,7 @@ "generateSuccess": "Imágenes de ejemplo generadas correctamente", "missingStatesWarning": { "title": "Faltan Ejemplos de Estado", - "description": "Se recomienda seleccionar ejemplos para todos los estados para obtener mejores resultados. Puede continuar sin seleccionar todos los estados, pero el modelo no se entrenará hasta que todos los estados tengan imágenes. Después de continuar, use la vista \"Clasificaciones recientes\" para clasificar las imágenes de los estados faltantes y luego entrene el modelo." + "description": "No todas las clases tienen ejemplos. Prueba a generar nuevos ejemplos para encontrar la clase que falta, o continúa y usa la vista de Clasificaciones recientes para añadir imágenes más tarde." }, "allImagesRequired_one": "Por favor clasifique todas las imágenes. Queda {{count}} imagen.", "allImagesRequired_many": "Por favor clasifique todas las imágenes. Quedan {{count}} imágenes.", diff --git a/web/public/locales/es/views/events.json b/web/public/locales/es/views/events.json index f2bdab0e99..7c2dd8b362 100644 --- a/web/public/locales/es/views/events.json +++ b/web/public/locales/es/views/events.json @@ -32,7 +32,9 @@ }, "camera": "Cámara", "recordings": { - "documentTitle": "Grabaciones - Frigate" + "documentTitle": "Grabaciones - Frigate", + "invalidSharedLink": "No se puede abrir el enlace de la grabación con marca de tiempo debido a un error de análisis.", + "invalidSharedCamera": "No se puede abrir el enlace de la grabación con marca de tiempo debido a una cámara desconocida o no autorizada." }, "calendarFilter": { "last24Hours": "Últimas 24 horas" @@ -66,5 +68,28 @@ "select_all": "Todas", "normalActivity": "Normal", "needsReview": "Necesita revisión", - "securityConcern": "Aviso de seguridad" + "securityConcern": "Aviso de seguridad", + "motionSearch": { + "menuItem": "Búsqueda de movimiento", + "openMenu": "Opciones de cámara" + }, + "motionPreviews": { + "menuItem": "Ver vistas previas de movimiento", + "title": "Vistas previas de movimiento: {{camera}}", + "mobileSettingsTitle": "Ajustes de vistas previas de movimiento", + "mobileSettingsDesc": "Ajusta la velocidad de reproducción y el atenuado, y elige una fecha para revisar clips solo de movimiento.", + "dim": "Atenuar", + "dimAria": "Ajustar intensidad de atenuado", + "dimDesc": "Aumenta el atenuado para mejorar la visibilidad de las áreas con movimiento.", + "speed": "Velocidad", + "speedAria": "Seleccionar velocidad de reproducción de las vistas previas", + "speedDesc": "Elige la velocidad a la que se reproducen los clips de vista previa.", + "back": "Atrás", + "empty": "No hay vistas previas disponibles", + "noPreview": "Vista previa no disponible", + "seekAria": "Mover el reproductor de {{camera}} a {{time}}", + "filter": "Filtrar", + "filterDesc": "Selecciona áreas para mostrar solo clips con movimiento en esas regiones.", + "filterClear": "Limpiar" + } } diff --git a/web/public/locales/es/views/explore.json b/web/public/locales/es/views/explore.json index ded5ca91fb..f6d61180fd 100644 --- a/web/public/locales/es/views/explore.json +++ b/web/public/locales/es/views/explore.json @@ -226,6 +226,10 @@ }, "more": { "aria": "Más" + }, + "debugReplay": { + "label": "Reproducción de depuración", + "aria": "Ver este objeto rastreado en la reproducción de depuración" } }, "dialog": { @@ -282,7 +286,10 @@ "zones": "Zonas", "area": "Área", "score": "Puntuación", - "ratio": "Ratio(proporción)" + "ratio": "Ratio(proporción)", + "computedScore": "Puntuación calculada", + "topScore": "Puntuación más alta", + "toggleAdvancedScores": "Alternar puntuaciones avanzadas" }, "entered_zone": "{{label}} ha entrado en {{zones}}" }, diff --git a/web/public/locales/es/views/exports.json b/web/public/locales/es/views/exports.json index cc2306da06..b464f3ab0d 100644 --- a/web/public/locales/es/views/exports.json +++ b/web/public/locales/es/views/exports.json @@ -13,7 +13,9 @@ "toast": { "error": { "renameExportFailed": "No se pudo renombrar la exportación: {{errorMessage}}", - "assignCaseFailed": "Fallo en la actualización de la asignación de caso: {{errorMessage}}" + "assignCaseFailed": "Fallo en la actualización de la asignación de caso: {{errorMessage}}", + "caseSaveFailed": "No se pudo guardar el caso: {{errorMessage}}", + "caseDeleteFailed": "No se pudo eliminar el caso: {{errorMessage}}" } }, "deleteExport.desc": "¿Estás seguro de que quieres eliminar {{exportName}}?", @@ -38,10 +40,89 @@ "descriptionLabel": "Descripción" }, "toolbar": { - "addExport": "Añadir Exportación" + "addExport": "Añadir Exportación", + "newCase": "Nuevo caso", + "editCase": "Editar caso", + "deleteCase": "Eliminar caso" }, "deleteCase": { "label": "Eliminar caso", - "desc": "¿Estás seguro de que quieres eliminar {{caseName}}?" + "desc": "¿Estás seguro de que quieres eliminar {{caseName}}?", + "descKeepExports": "Las exportaciones seguirán disponibles como exportaciones sin categoría.", + "descDeleteExports": "Todas las exportaciones de este caso se eliminarán de forma permanente.", + "deleteExports": "Eliminar también las exportaciones" + }, + "caseCard": { + "emptyCase": "Aún no hay exportaciones" + }, + "jobCard": { + "defaultName": "Exportación de {{camera}}", + "queued": "En cola", + "running": "En ejecución", + "preparing": "Preparando", + "copying": "Copiando", + "encoding": "Codificando", + "encodingRetry": "Codificando (reintento)", + "finalizing": "Finalizando" + }, + "caseView": { + "noDescription": "Sin descripción", + "createdAt": "Creado {{value}}", + "exportCount_one": "1 exportación", + "exportCount_other": "{{count}} exportaciones", + "cameraCount_one": "1 cámara", + "cameraCount_other": "{{count}} cámaras", + "showMore": "Mostrar más", + "showLess": "Mostrar menos", + "emptyTitle": "Este caso está vacío", + "emptyDescription": "Añade exportaciones existentes sin categorizar para mantener el caso organizado.", + "emptyDescriptionNoExports": "Todavía no hay exportaciones sin categorizar disponibles para añadir." + }, + "caseEditor": { + "createTitle": "Crear caso", + "editTitle": "Editar caso", + "namePlaceholder": "Nombre del caso", + "descriptionPlaceholder": "Añade notas o contexto para este caso" + }, + "addExportDialog": { + "title": "Añadir exportación a {{caseName}}", + "searchPlaceholder": "Buscar exportaciones sin categorizar", + "empty": "Ninguna exportación sin categorizar coincide con esta búsqueda.", + "addButton_one": "Añadir 1 exportación", + "addButton_other": "Añadir {{count}} exportaciones", + "adding": "Añadiendo..." + }, + "selected_one": "{{count}} seleccionados", + "selected_other": "{{count}} seleccionados", + "bulkActions": { + "addToCase": "Añadir al caso", + "moveToCase": "Mover al caso", + "removeFromCase": "Eliminar del caso", + "delete": "Eliminar", + "deleteNow": "Eliminar ahora" + }, + "bulkDelete": { + "title": "Eliminar exportaciones", + "desc_one": "¿Seguro que quieres eliminar {{count}} exportación?", + "desc_other": "¿Seguro que quieres eliminar {{count}} exportaciones?" + }, + "bulkRemoveFromCase": { + "title": "Eliminar del caso", + "desc_one": "¿Eliminar {{count}} exportación de este caso?", + "desc_other": "¿Eliminar {{count}} exportaciones de este caso?", + "descKeepExports": "Las exportaciones se moverán a sin categorizar.", + "descDeleteExports": "Las exportaciones se eliminarán permanentemente.", + "deleteExports": "Eliminar exportaciones en su lugar" + }, + "bulkToast": { + "success": { + "delete": "Exportaciones eliminadas correctamente", + "reassign": "Asignación de caso actualizada correctamente", + "remove": "Exportaciones eliminadas del caso correctamente" + }, + "error": { + "deleteFailed": "No se pudieron eliminar las exportaciones: {{errorMessage}}", + "reassignFailed": "No se pudo actualizar la asignación del caso: {{errorMessage}}" + } } } diff --git a/web/public/locales/es/views/faceLibrary.json b/web/public/locales/es/views/faceLibrary.json index f923082dac..8014830fae 100644 --- a/web/public/locales/es/views/faceLibrary.json +++ b/web/public/locales/es/views/faceLibrary.json @@ -30,7 +30,11 @@ "title": "Reconocimientos Recientes", "aria": "Seleccionar reconocimientos recientes", "empty": "No hay intentos recientes de reconocimiento facial", - "titleShort": "Reciente" + "titleShort": "Reciente", + "emptyNoLibrary": { + "title": "Subir una cara", + "description": "Debes añadir al menos una cara a la biblioteca para que el reconocimiento facial funcione." + } }, "selectItem": "Seleccionar {{item}}", "selectFace": "Seleccionar rostro", diff --git a/web/public/locales/es/views/live.json b/web/public/locales/es/views/live.json index 4bc98b5cbc..2052b3698f 100644 --- a/web/public/locales/es/views/live.json +++ b/web/public/locales/es/views/live.json @@ -69,7 +69,8 @@ }, "recording": { "enable": "Habilitar grabación", - "disable": "Deshabilitar grabación" + "disable": "Deshabilitar grabación", + "disabledInConfig": "La grabación debe activarse primero en Ajustes para esta cámara." }, "snapshots": { "enable": "Habilitar capturas de pantalla", diff --git a/web/public/locales/es/views/motionSearch.json b/web/public/locales/es/views/motionSearch.json index 0967ef424b..45b1ddca90 100644 --- a/web/public/locales/es/views/motionSearch.json +++ b/web/public/locales/es/views/motionSearch.json @@ -1 +1,77 @@ -{} +{ + "documentTitle": "Búsqueda por movimiento - Frigate", + "title": "Búsqueda por movimiento", + "description": "Dibuja un polígono para definir la región de interés y especifica un intervalo de tiempo para buscar cambios de movimiento dentro de esa región.", + "selectCamera": "Búsqueda por movimiento se está cargando", + "startSearch": "Iniciar búsqueda", + "searchStarted": "Búsqueda iniciada", + "searchCancelled": "Búsqueda cancelada", + "cancelSearch": "Cancelar", + "searching": "Búsqueda en progreso.", + "searchComplete": "Búsqueda completada", + "noResultsYet": "Ejecuta una búsqueda para encontrar cambios de movimiento en la región seleccionada", + "noChangesFound": "No se detectaron cambios de píxeles en la región seleccionada", + "changesFound_one": "Encontrado {{count}} cambio de movimiento", + "changesFound_many": "Encontrados {{count}} cambios de movimiento", + "changesFound_other": "Encontrados {{count}} cambios de movimiento", + "framesProcessed": "{{count}} fotogramas procesados", + "jumpToTime": "Saltar a este tiempo", + "results": "Resultados", + "showSegmentHeatmap": "Mapa de calor", + "newSearch": "Nueva búsqueda", + "clearResults": "Borrar resultados", + "clearROI": "Borrar polígono", + "polygonControls": { + "points_one": "{{count}} punto", + "points_many": "{{count}} puntos", + "points_other": "{{count}} puntos", + "undo": "Deshacer el último punto", + "reset": "Restablecer polígono" + }, + "motionHeatmapLabel": "Mapa de calor de movimiento", + "dialog": { + "title": "Búsqueda de movimiento", + "cameraLabel": "Cámara", + "previewAlt": "Vista previa de la cámara {{camera}}" + }, + "timeRange": { + "title": "Rango de búsqueda", + "start": "Hora de inicio", + "end": "Hora de finalización" + }, + "settings": { + "title": "Ajustes de búsqueda", + "parallelMode": "Modo paralelo", + "parallelModeDesc": "Analiza varios segmentos de grabación al mismo tiempo (más rápido, pero consume significativamente más CPU)", + "threshold": "Umbral de sensibilidad", + "thresholdDesc": "Los valores más bajos detectan cambios más pequeños (1-255)", + "minArea": "Área mínima de cambio", + "minAreaDesc": "Porcentaje mínimo de la región de interés que debe cambiar para considerarse significativo", + "frameSkip": "Salto de fotogramas", + "frameSkipDesc": "Procesa cada N fotogramas. Establécelo según la tasa de FPS de tu cámara para procesar un fotograma por segundo (p. ej., 5 para una cámara de 5 FPS, 30 para una cámara de 30 FPS). Los valores más altos serán más rápidos, pero pueden omitir eventos de movimiento breves.", + "maxResults": "Resultados máximos", + "maxResultsDesc": "Detener después de esta cantidad de marcas de tiempo coincidentes" + }, + "errors": { + "noCamera": "Selecciona una cámara", + "noROI": "Dibuja una región de interés", + "noTimeRange": "Selecciona un rango de tiempo", + "invalidTimeRange": "La hora de fin debe ser posterior a la hora de inicio", + "searchFailed": "La búsqueda falló: {{message}}", + "polygonTooSmall": "El polígono debe tener al menos 3 puntos", + "unknown": "Error desconocido" + }, + "changePercentage": "{{percentage}}% cambiado", + "metrics": { + "title": "Métricas de búsqueda", + "segmentsScanned": "Segmentos analizados", + "segmentsProcessed": "Procesado", + "segmentsSkippedInactive": "Omitido (sin actividad)", + "segmentsSkippedHeatmap": "Omitido (sin superposición de ROI)", + "fallbackFullRange": "Análisis completo de respaldo", + "framesDecoded": "Fotogramas decodificados", + "wallTime": "Tiempo de búsqueda", + "segmentErrors": "Errores de segmento", + "seconds": "{{seconds}} s" + } +} diff --git a/web/public/locales/es/views/replay.json b/web/public/locales/es/views/replay.json index 0967ef424b..f1b7a84f97 100644 --- a/web/public/locales/es/views/replay.json +++ b/web/public/locales/es/views/replay.json @@ -1 +1,59 @@ -{} +{ + "title": "Depuración de reproducción", + "description": "Reproducir grabaciones de cámara para depuración. La lista de objetos muestra un resumen con retraso temporal de los objetos detectados y la pestaña Mensajes muestra un flujo de los mensajes internos de Frigate de la grabación reproducida.", + "websocket_messages": "Mensajes", + "dialog": { + "title": "Iniciar depuración de reproducción", + "description": "Crea una cámara de reproducción temporal que reproduzca en bucle imágenes históricas para depurar problemas de detección y seguimiento de objetos. La cámara de reproducción tendrá la misma configuración de detección que la cámara de origen. Elige un intervalo de tiempo para comenzar.", + "camera": "Cámara de origen", + "timeRange": "Intervalo de tiempo", + "preset": { + "1m": "Último 1 minuto", + "5m": "Últimos 5 minutos", + "timeline": "Desde la línea de tiempo", + "custom": "Personalizado" + }, + "startButton": "Iniciar reproducción", + "selectFromTimeline": "Seleccionar", + "starting": "Iniciando reproducción...", + "startLabel": "Iniciar", + "endLabel": "Fin", + "toast": { + "error": "No se pudo iniciar la reproducción de depuración: {{error}}", + "alreadyActive": "Ya hay una sesión de reproducción activa", + "stopError": "No se pudo detener la reproducción de depuración: {{error}}", + "goToReplay": "Ir a la reproducción" + } + }, + "page": { + "noSession": "No hay ninguna sesión activa de reproducción de depuración", + "noSessionDesc": "Inicia una reproducción de depuración desde la vista Historial haciendo clic en el botón Acciones de la barra de herramientas y seleccionando Reproducción de depuración.", + "goToRecordings": "Ir al historial", + "preparingClip": "Preparando clip…", + "preparingClipDesc": "Frigate está uniendo las grabaciones del intervalo de tiempo seleccionado. Esto puede tardar un minuto en intervalos más largos.", + "startingCamera": "Iniciando reproducción de depuración…", + "startError": { + "title": "No se pudo iniciar la reproducción de depuración", + "back": "Volver al historial" + }, + "sourceCamera": "Cámara de origen", + "replayCamera": "Cámara de reproducción", + "initializingReplay": "Inicializando reproducción de depuración…", + "stoppingReplay": "Deteniendo repetición de depuración...", + "stopReplay": "Detener repetición", + "confirmStop": { + "title": "¿Detener repetición de depuración?", + "description": "Esto detendrá la sesión y eliminará todos los datos temporales. ¿Estás seguro?", + "confirm": "Detener repetición", + "cancel": "Cancelar" + }, + "activity": "Actividad", + "objects": "Lista de objetos", + "audioDetections": "Detecciones de audio", + "noActivity": "No se detectó actividad", + "activeTracking": "Seguimiento activo", + "noActiveTracking": "No hay seguimiento activo", + "configuration": "Configuración", + "configurationDesc": "Ajusta con precisión la detección de movimiento y los ajustes de seguimiento de objetos para la cámara de repetición de depuración. No se guardará ningún cambio en el archivo de configuración de Frigate." + } +} diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json index 075b7131ff..7dc10c8a63 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -16,7 +16,8 @@ "globalConfig": "Configuración Global - Frigate", "cameraConfig": "Configuración de Cámara - Frigate", "maintenance": "Mantenimiento - Frigate", - "profiles": "Perfiles - Frigate" + "profiles": "Perfiles - Frigate", + "detectorsAndModel": "Detectores y modelo - Frigate" }, "menu": { "cameras": "Configuración de Cámara", @@ -42,7 +43,7 @@ "globalDetect": "Detección de Objetos", "globalRecording": "Grabación", "globalSnapshots": "Instantáneas", - "globalFfmpeg": "FFmpeg", + "globalFfmpeg": "arguments,Introduce", "globalMotion": "Detección de Movimiento", "globalObjects": "Objetos", "globalReview": "Revisión", @@ -50,7 +51,49 @@ "globalLivePlayback": "Reproducción en Vivo", "globalTimestampStyle": "Estilo de Marca de Tiempo", "systemDatabase": "Base de Datos", - "systemAuthentication": "Autenticación" + "systemAuthentication": "Autenticación", + "systemTls": "TLS", + "systemNetworking": "Red", + "systemProxy": "Proxy", + "systemUi": "Interfaz", + "systemLogging": "Registro", + "systemEnvironmentVariables": "Variables de entorno", + "systemTelemetry": "Telemetría", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorHardware": "Hardware del detector", + "systemDetectionModel": "Modelo de detección", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Flujos go2rtc", + "integrationSemanticSearch": "Búsqueda semántica", + "integrationGenerativeAi": "IA generativa", + "integrationFaceRecognition": "Reconocimiento facial", + "integrationLpr": "Reconocimiento de matrículas", + "integrationObjectClassification": "Clasificación de objetos", + "integrationAudioTranscription": "Transcripción de audio", + "cameraDetect": "Detección de objetos", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "Grabación", + "cameraSnapshots": "Instantáneas", + "cameraMotion": "Detección de movimiento", + "cameraObjects": "Objetos", + "cameraConfigReview": "Revisión", + "cameraAudioEvents": "Detección de audio", + "cameraAudioTranscription": "Transcripción de audio", + "cameraNotifications": "Notificaciones", + "cameraLivePlayback": "Reproducción en directo", + "cameraBirdseye": "Birdseye", + "cameraFaceRecognition": "Reconocimiento facial", + "cameraLpr": "Reconocimiento de matrículas", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "Interfaz de cámara", + "cameraTimestampStyle": "Estilo de marca de tiempo", + "cameraMqtt": "MQTT de cámara", + "maintenance": "Mantenimiento", + "mediaSync": "Sincronización de medios", + "regionGrid": "Cuadrícula de regiones", + "systemDetectorsAndModel": "Detectores y modelo" }, "dialog": { "unsavedChanges": { @@ -59,7 +102,7 @@ } }, "cameraSetting": { - "camera": "Cámara", + "camera": "Overrides,Sobrescrituras", "noCamera": "Sin cámara" }, "general": { @@ -303,6 +346,10 @@ "zone": "zona", "motion_mask": "máscara de movimiento", "object_mask": "máscara de objeto" + }, + "revertOverride": { + "title": "Revertir a la configuración base", + "desc": "Esto eliminará la sobrescritura del perfil para {{type}} {{name}} y revertirá a la configuración base." } }, "speed": { @@ -314,6 +361,12 @@ "error": { "mustNotBeEmpty": "El nombre no puede estar vacío." } + }, + "id": { + "error": { + "mustNotBeEmpty": "El ID no puede estar vacío.", + "alreadyExists": "Ya existe una máscara con este ID para esta cámara." + } } }, "zones": { @@ -370,7 +423,8 @@ "success": "La zona ({{zoneName}}) ha sido guardada." }, "enabled": { - "description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución." + "description": "Indica si esta zona está activa y habilitada en la configuración. Si está deshabilitado, no puede ser habilitado por MQTT. Las zonas deshabilitadas se ignoran durante la ejecución.", + "title": "Habilitado" } }, "toast": { @@ -411,7 +465,13 @@ "documentTitle": "Editar Máscara de Movimiento - Frigate", "point_one": "{{count}} punto", "point_many": "{{count}} puntos", - "point_other": "{{count}} puntos" + "point_other": "{{count}} puntos", + "defaultName": "Máscara de movimiento {{number}}", + "name": { + "title": "Nombre", + "description": "Un nombre descriptivo opcional para esta máscara de movimiento.", + "placeholder": "Introduce un nombre..." + } }, "objectMasks": { "label": "Máscaras de Objetos", @@ -437,11 +497,26 @@ "point_one": "{{count}} punto", "point_many": "{{count}} puntos", "point_other": "{{count}} puntos", - "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen." + "clickDrawPolygon": "Haz clic para dibujar un polígono en la imagen.", + "name": { + "title": "Nombre", + "description": "Un nombre descriptivo opcional para esta máscara de objeto.", + "placeholder": "Introduce un nombre..." + } }, "restart_required": "Es necesario reiniciar (se han cambiado las máscaras/zonas)", "motionMaskLabel": "Máscara de movimiento {{number}}", - "objectMaskLabel": "Máscara de objeto {{number}}" + "objectMaskLabel": "Máscara de objeto {{number}}", + "disabledInConfig": "El elemento está deshabilitado en el archivo de configuración", + "addDisabledProfile": "Añádelo primero a la configuración base y luego sobrescríbelo en el perfil", + "profileBase": "(base)", + "profileOverride": "(sobrescritura)", + "masks": { + "enabled": { + "title": "Habilitado", + "description": "Indica si esta máscara está habilitada en el archivo de configuración. Si está deshabilitada, no se puede habilitar mediante MQTT. Las máscaras deshabilitadas se ignoran en tiempo de ejecución." + } + } }, "motionDetectionTuner": { "title": "Sintonizador de Detección de Movimiento", @@ -714,7 +789,7 @@ "snapshots": "Instantáneas", "cleanCopySnapshots": "clean_copy Instantáneas" }, - "desc": "Enviar a Frigate+ requiere que tanto las capturas instantáneas como las capturas clean_copy estén habilitadas en tu configuración.", + "desc": "Enviar a Frigate+ requiere que las instantáneas estén habilitadas en tu configuración.", "cleanCopyWarning": "Algunas cámaras tienen las instantáneas deshabilitadas" }, "modelInfo": { @@ -726,13 +801,21 @@ "cameras": "Cámaras", "loading": "Cargando información del modelo…", "error": "No se pudo cargar la información del modelo", - "availableModels": "Modelos disponibles", + "availableModels": "Modelos de Frigate+ disponibles", "loadingAvailableModels": "Cargando modelos disponibles…", "modelSelect": "Tus modelos disponibles en Frigate+ se pueden seleccionar aquí. Ten en cuenta que solo se pueden seleccionar modelos compatibles con tu configuración actual de detectores.", "trainDate": "Fecha de entrenamiento", "plusModelType": { "baseModel": "Modelo Base", "userModel": "Ajustado Finamente" + }, + "noModelLoaded": "Actualmente no hay ningún modelo de Frigate+ cargado.", + "selectModel": "Selecciona un modelo", + "noModelsAvailable": "No hay modelos disponibles", + "filter": { + "ariaLabel": "Filtrar modelos por tipo", + "baseModels": "Modelos base", + "fineTunedModels": "Modelos ajustados" } }, "toast": { @@ -741,7 +824,14 @@ }, "restart_required": "Es necesario reiniciar (se ha cambiado el modelo Frigate+)", "unsavedChanges": "Cambios en la configuración de Frigate+ no guardados", - "description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí." + "description": "Frigate+ es un servicio de suscripción que proporciona acceso a funciones y capacidades adicionales para su instancia de Frigate, incluida la posibilidad de utilizar modelos de detección de objetos personalizados entrenados con sus propios datos. Puede gestionar la configuración de sus modelos de Frigate+ aquí.", + "cardTitles": { + "api": "API", + "currentModel": "Modelo actual", + "otherModels": "Otros modelos", + "configuration": "Configuración" + }, + "changeInDetectorsAndModel": "Cambiar modelo" }, "enrichments": { "title": "Configuración de Enriquecimientos", @@ -767,11 +857,11 @@ "modelSize": { "label": "Tamaño del Modelo", "small": { - "title": "pequeño", + "title": "size", "desc": "Usar la opción small emplea una versión cuantizada del modelo que consume menos memoria RAM y se ejecuta más rápido en la CPU, con una diferencia muy pequeña o casi imperceptible en la calidad de las representaciones (embeddings)." }, "large": { - "title": "grande", + "title": "model", "desc": "Usar la opción large emplea el modelo completo de Jina y se ejecutará automáticamente en la GPU, si está disponible." }, "desc": "Tamaño del modelo usado para la búsqueda semántica." @@ -1157,7 +1247,8 @@ }, "hikvision": { "substreamWarning": "La subtransmisión 1 está limitada a una resolución baja. Muchas cámaras Hikvision admiten subtransmisiones adicionales que deben habilitarse en la configuración de la cámara. Se recomienda comprobar y utilizar dichas transmisiones si están disponibles." - } + }, + "resolutionUnknown": "No se pudo detectar la resolución de este flujo. Debes establecer manualmente la resolución de detección en Ajustes o en tu configuración." } }, "title": "Añadir cámara", @@ -1192,7 +1283,20 @@ "streams": { "title": "Habilitar/deshabilitar cámaras", "desc": "Desactiva temporalmente una cámara hasta que Frigate se reinicie. Desactivar una cámara detiene por completo el procesamiento de las transmisiones de Frigate. La detección, la grabación y la depuración no estarán disponibles.
    Nota: Esto no desactiva las retransmisiones de go2rtc.", - "enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene por completo el procesamiento de las transmisiones de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles.
    Nota: Esto no deshabilita las retransmisiones de go2rtc." + "enableDesc": "Deshabilita temporalmente una cámara habilitada hasta que Frigate se reinicie. Deshabilitar una cámara detiene completamente el procesamiento de los flujos de esa cámara por parte de Frigate. La detección, la grabación y la depuración no estarán disponibles. Nota: Esto no deshabilita las retransmisiones de go2rtc.Arrastra el controlador para reordenar las cámaras tal y como aparecen en la interfaz. El orden de las cámaras habilitadas se reflejará en toda la interfaz, incluido el panel en directo y los menús desplegables de selección de cámaras.", + "enableLabel": "Cámaras habilitadas", + "disableLabel": "Cámaras deshabilitadas", + "disableDesc": "Habilita una cámara que actualmente no está visible en la interfaz y está deshabilitada en la configuración. Es necesario reiniciar Frigate después de habilitarla.", + "enableSuccess": "{{cameraName}} se ha habilitado en la configuración. Reinicia Frigate para aplicar los cambios.", + "friendlyName": { + "edit": "Editar nombre visible de la cámara", + "title": "Editar nombre visible", + "description": "Establece el nombre descriptivo que se mostrará para esta cámara en toda la interfaz de Frigate. Déjalo en blanco para usar el ID de la cámara.", + "rename": "Renombrar" + }, + "reorderHandle": "Arrastrar para reordenar", + "saving": "Guardando…", + "saved": "Guardado" }, "cameraConfig": { "add": "Añadir cámara", @@ -1224,8 +1328,34 @@ } }, "deleteCameraDialog": { - "description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara." - } + "description": "Eliminar una cámara borrará permanentemente todas las grabaciones, los objetos rastreados y la configuración de esa cámara. Es posible que sea necesario eliminar manualmente cualquier transmisión go2rtc asociada a esta cámara.", + "title": "Eliminar cámara", + "selectPlaceholder": "Elegir cámara...", + "confirmTitle": "¿Estás seguro?", + "confirmWarning": "Eliminar {{cameraName}} no se puede deshacer.", + "deleteExports": "Eliminar también las exportaciones de esta cámara", + "confirmButton": "Eliminar permanentemente", + "success": "La cámara {{cameraName}} se ha eliminado correctamente", + "error": "No se pudo eliminar la cámara {{cameraName}}" + }, + "deleteCamera": "Eliminar cámara", + "profiles": { + "title": "Sobrescrituras de cámaras del perfil", + "selectLabel": "Seleccionar perfil", + "description": "Configura qué cámaras se habilitan o deshabilitan cuando se activa un perfil. Las cámaras configuradas como \"Heredar\" conservan su estado base habilitado.", + "inherit": "Heredar", + "enabled": "Habilitado", + "disabled": "Deshabilitado" + }, + "cameraType": { + "title": "Tipo de cámara", + "label": "Tipo de cámara", + "description": "Establece el tipo de cada cámara. Las cámaras LPR dedicadas son cámaras de un solo propósito con un zoom óptico potente para capturar matrículas de vehículos lejanos. La mayoría de cámaras deberían usar el tipo de cámara normal salvo que la cámara esté específicamente destinada a LPR y tenga una vista muy enfocada a matrículas.", + "normal": "Normal", + "dedicatedLpr": "LPR dedicada", + "saveSuccess": "Se ha actualizado el tipo de cámara de {{cameraName}}. Reinicia Frigate para aplicar los cambios." + }, + "description": "Añade, edita y elimina cámaras, controla qué cámaras están habilitadas y configura sobrescrituras por perfil y tipo de cámara. Para configurar flujos, detección, movimiento y otros ajustes específicos de cámara, selecciona la sección correspondiente dentro de Configuración de cámara." }, "cameraReview": { "title": "Configuración de revisión de la cámara", @@ -1268,34 +1398,295 @@ "overriddenGlobal": "Sobrescrito (Global)", "overriddenBaseConfigTooltip": "El perfil {{profile}} sobrescribe los ajustes de configuración de esta sección", "overriddenGlobalTooltip": "Esta cámara sobrescribe los ajustes de configuración global en esta sección", - "overriddenBaseConfig": "Sobrescrito (Configuración Base)" + "overriddenBaseConfig": "Sobrescrito (Configuración Base)", + "overriddenInCameras": { + "label_one": "Sobrescrito en {{count}} cámara", + "label_many": "Sobrescrito en {{count}} cámaras", + "label_other": "Sobrescrito en {{count}} cámaras", + "tooltip_one": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "tooltip_many": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "tooltip_other": "{{count}} cámaras sobrescriben los valores de esta sección. Haz clic para ver los detalles.", + "heading_one": "This global section has fields that are overridden in {{count}} camera.", + "heading_many": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.", + "heading_other": "Esta sección global tiene campos que están sobrescritos en {{count}} cámaras.", + "othersField_one": "{{count}} más", + "othersField_many": "{{count}} más", + "othersField_other": "{{count}} más", + "profilePrefix": "Perfil {{profile}}: {{fields}}" + }, + "overriddenGlobalHeading_one": "Esta cámara sobrescribe {{count}} campo de la configuración global:", + "overriddenGlobalHeading_many": "Esta cámara sobrescribe {{count}} campos de la configuración global:", + "overriddenGlobalHeading_other": "Esta cámara sobrescribe {{count}} campos de la configuración global:", + "overriddenGlobalNoDeltas": "Esta cámara sobrescribe la configuración global, pero no hay diferencias en los valores de los campos.", + "overriddenBaseConfigHeading_one": "El perfil {{profile}} sobrescribe {{count}} campo de la configuración base:", + "overriddenBaseConfigHeading_many": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:", + "overriddenBaseConfigHeading_other": "El perfil {{profile}} sobrescribe {{count}} campos de la configuración base:", + "overriddenBaseConfigNoDeltas": "El perfil {{profile}} sobrescribe esta sección, pero no hay diferencias en los valores de los campos respecto a la configuración base." }, "onvif": { - "profileLoading": "Cargando perfiles..." + "profileLoading": "Cargando perfiles...", + "profileAuto": "Auto", + "autotracking": { + "zooming": { + "disabled": "Deshabilitado", + "absolute": "Absoluto", + "relative": "Relativo" + } + } }, "maintenance": { "sync": { "verboseDesc": "Escribe una lista completa de archivos huérfanos en el disco para su revisión.", "verbose": "Detallado", "desc": "Frigate limpiará periódicamente los archivos multimedia según un cronograma regular, de acuerdo con su configuración de retención. Es normal ver algunos archivos huérfanos mientras Frigate se ejecuta. Utilice esta función para eliminar del disco los archivos multimedia huérfanos que ya no se referencian en la base de datos.", - "forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos." + "forceDesc": "Omitir el umbral de seguridad y completar la sincronización incluso si se eliminara más del 50% de los archivos.", + "title": "Sincronización de medios", + "started": "Sincronización de medios iniciada.", + "alreadyRunning": "Ya hay una tarea de sincronización en ejecución", + "error": "No se pudo iniciar la sincronización", + "currentStatus": "Estado", + "jobId": "ID de tarea", + "startTime": "Hora de inicio", + "endTime": "Hora de finalización", + "statusLabel": "Estado", + "results": "Resultados", + "errorLabel": "Error", + "mediaTypes": "Tipos de medios", + "allMedia": "Todos los medios", + "dryRun": "Simulación", + "dryRunEnabled": "No se eliminará ningún archivo", + "dryRunDisabled": "Se eliminarán archivos", + "force": "Forzar", + "running": "Sincronización en curso...", + "start": "Iniciar sincronización", + "inProgress": "La sincronización está en curso. Esta página está deshabilitada.", + "status": { + "queued": "En cola", + "running": "En ejecución", + "completed": "Completado", + "failed": "Fallido", + "notRunning": "No está en ejecución" + }, + "resultsFields": { + "filesChecked": "Archivos comprobados", + "orphansFound": "Huérfanos encontrados", + "orphansDeleted": "Huérfanos eliminados", + "aborted": "Abortado. La eliminación superaría el umbral de seguridad.", + "error": "Error", + "totals": "Totales" + }, + "event_snapshots": "Instantáneas de objetos rastreados", + "event_thumbnails": "Miniaturas de objetos rastreados", + "review_thumbnails": "Miniaturas de revisión", + "previews": "Vistas previas", + "exports": "Exportaciones", + "recordings": "Grabaciones" }, "regionGrid": { "clearConfirmDesc": "No se recomienda borrar la cuadrícula de la región a menos que haya cambiado recientemente el tamaño del modelo de su detector o la posición física de su cámara y esté experimentando problemas de seguimiento de objetos. La cuadrícula se reconstruirá automáticamente con el tiempo a medida que se realice el seguimiento de los objetos. Es necesario reiniciar Frigate para que los cambios surtan efecto.", - "desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados." - } + "desc": "La cuadrícula de regiones es una optimización que aprende dónde suelen aparecer los objetos de diferentes tamaños en el campo de visión de cada cámara. Frigate utiliza estos datos para dimensionar de forma eficiente las regiones de detección. La cuadrícula se construye automáticamente a lo largo del tiempo a partir de los datos de los objetos rastreados.", + "title": "Cuadrícula de regiones", + "clear": "Borrar cuadrícula de regiones", + "clearConfirmTitle": "Borrar cuadrícula de regiones", + "clearSuccess": "Cuadrícula de regiones borrada correctamente", + "clearError": "No se pudo borrar la cuadrícula de regiones", + "restartRequired": "Es necesario reiniciar para que los cambios de la cuadrícula de regiones surtan efecto" + }, + "title": "Mantenimiento" }, "configForm": { "camera": { "noCameras": "No hay cámaras disponibles", - "description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales." + "description": "Estos ajustes se aplican únicamente a esta cámara y anulan los ajustes globales.", + "title": "Ajustes de cámara" }, "genaiModel": { - "noModels": "No hay modelos disponibles" + "noModels": "No hay modelos disponibles", + "placeholder": "Seleccionar modelo…", + "search": "Buscar modelos…" }, "global": { - "description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara." - } + "description": "Estos ajustes se aplican a todas las cámaras, a menos que se anulen en los ajustes específicos de cada cámara.", + "title": "Ajustes globales" + }, + "sections": { + "go2rtc": "streams", + "detect": "Detección", + "record": "Grabación", + "snapshots": "Instantáneas", + "motion": "Movimiento", + "objects": "Objetos", + "review": "Revisión", + "audio": "Audio", + "notifications": "Notificaciones", + "live": "Vista en directo", + "timestamp_style": "Marcas de tiempo", + "mqtt": "MQTT", + "database": "Base de datos", + "telemetry": "Telemetría", + "auth": "Autenticación", + "tls": "TLS", + "proxy": "Proxy", + "ffmpeg": "FFmpeg", + "detectors": "Detectores", + "model": "Modelo", + "semantic_search": "Búsqueda semántica", + "genai": "GenAI", + "face_recognition": "Reconocimiento facial", + "lpr": "Reconocimiento de matrículas", + "birdseye": "Birdseye", + "masksAndZones": "Máscaras / zonas" + }, + "advancedSettingsCount": "Ajustes avanzados ({{count}})", + "advancedCount": "Avanzado ({{count}})", + "showAdvanced": "Mostrar ajustes avanzados", + "tabs": { + "sharedDefaults": "Valores predeterminados compartidos", + "system": "Sistema", + "integrations": "Integraciones" + }, + "additionalProperties": { + "keyLabel": "Clave", + "valueLabel": "Valor", + "keyPlaceholder": "Nueva clave", + "remove": "Eliminar" + }, + "knownPlates": { + "namePlaceholder": "p. ej., Coche de mi mujer", + "platePlaceholder": "Número de matrícula o regex" + }, + "timezone": { + "defaultOption": "Usar zona horaria del navegador" + }, + "roleMap": { + "empty": "No hay asignaciones de roles", + "roleLabel": "Rol", + "groupsLabel": "Grupos", + "addMapping": "Añadir asignación de rol", + "remove": "Eliminar" + }, + "ffmpegArgs": { + "preset": "Preajuste", + "manual": "Argumentos manuales", + "inherit": "Heredar del ajuste de cámara", + "none": "Ninguno", + "useGlobalSetting": "Heredar del ajuste global", + "selectPreset": "Seleccionar preajuste", + "manualPlaceholder": "Introduce argumentos de FFmpeg", + "presetLabels": { + "preset-rpi-64-h264": "Raspberry Pi (H.264)", + "preset-rpi-64-h265": "Raspberry Pi (H.265)", + "preset-vaapi": "VAAPI (GPU Intel/AMD)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "GPU NVIDIA", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "Rockchip RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG (genérico)", + "preset-http-mjpeg-generic": "HTTP MJPEG (genérico)", + "preset-http-reolink": "HTTP - Cámaras Reolink", + "preset-rtmp-generic": "RTMP (genérico)", + "preset-rtsp-generic": "RTSP (genérico)", + "preset-rtsp-restream": "RTSP - Retransmisión desde go2rtc", + "preset-rtsp-restream-low-latency": "RTSP - Retransmisión desde go2rtc (baja latencia)", + "preset-rtsp-udp": "RTSP - UDP", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "Grabación (genérica, sin audio)", + "preset-record-generic-audio-copy": "Grabación (genérica + copiar audio)", + "preset-record-generic-audio-aac": "Grabación (genérica + audio a AAC)", + "preset-record-mjpeg": "Grabación - Cámaras MJPEG", + "preset-record-jpeg": "Grabación - Cámaras JPEG", + "preset-record-ubiquiti": "Grabación - Cámaras Ubiquiti" + } + }, + "cameraInputs": { + "itemTitle": "Flujo {{index}}" + }, + "restartRequiredField": "Reinicio necesario", + "restartRequiredFooter": "Configuración modificada - reinicio necesario", + "detect": { + "title": "Ajustes de detección" + }, + "detectors": { + "title": "Ajustes de detector", + "singleType": "Solo se permite un detector {{type}}.", + "keyRequired": "El nombre del detector es obligatorio.", + "keyDuplicate": "El nombre del detector ya existe.", + "noSchema": "No hay esquemas de detector disponibles.", + "none": "No hay instancias de detector configuradas.", + "add": "Añadir detector", + "addCustomKey": "Añadir clave personalizada" + }, + "record": { + "title": "Ajustes de grabación" + }, + "snapshots": { + "title": "Ajustes de instantáneas" + }, + "motion": { + "title": "Ajustes de movimiento" + }, + "objects": { + "title": "Ajustes de objetos" + }, + "audioLabels": { + "summary": "{{count}} etiquetas de audio seleccionadas", + "empty": "No hay etiquetas de audio disponibles" + }, + "objectLabels": { + "summary": "{{count}} tipos de objeto seleccionados", + "empty": "No hay etiquetas de objeto disponibles" + }, + "reviewLabels": { + "summary": "{{count}} etiquetas seleccionadas", + "empty": "No hay etiquetas disponibles" + }, + "filters": { + "objectFieldLabel": "{{field}} para {{label}}" + }, + "zoneNames": { + "summary": "{{count}} seleccionados", + "empty": "No hay zonas disponibles" + }, + "inputRoles": { + "summary": "{{count}} roles seleccionados", + "empty": "No hay roles disponibles", + "options": { + "detect": "Detectar", + "record": "Grabar", + "audio": "Audio" + } + }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "descriptions": "Descripciones", + "chat": "Chat" + } + }, + "semanticSearchModel": { + "placeholder": "Seleccionar modelo…", + "builtIn": "Modelos integrados", + "genaiProviders": "Proveedores de GenAI" + }, + "review": { + "title": "Ajustes de revisión" + }, + "audio": { + "title": "Ajustes de audio" + }, + "notifications": { + "title": "Ajustes de notificaciones" + }, + "live": { + "title": "Ajustes de vista en directo" + }, + "timestamp_style": { + "title": "Ajustes de marcas de tiempo" + }, + "searchPlaceholder": "Buscar...", + "addCustomLabel": "Añadir etiqueta personalizada..." }, "globalConfig": { "title": "Configuración global", @@ -1330,7 +1721,10 @@ "saveAllPartial_one": "Se ha guardado {{successCount}} de {{totalCount}} sección. {{failCount}} ha fallado.", "saveAllPartial_many": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.", "saveAllPartial_other": "Se han guardado {{successCount}} de {{totalCount}} secciones. {{failCount}} han fallado.", - "saveAllFailure": "Error al guardar todas las secciones." + "saveAllFailure": "Error al guardar todas las secciones.", + "saveAllSuccessRestartRequired_one": "La sección {{count}} se ha guardado correctamente. Reinicia Frigate para aplicar los cambios.", + "saveAllSuccessRestartRequired_many": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios.", + "saveAllSuccessRestartRequired_other": "Las {{count}} secciones se han guardado correctamente. Reinicia Frigate para aplicar los cambios." }, "profiles": { "title": "Perfiles", @@ -1364,26 +1758,84 @@ "renameProfile": "Renombrar perfil", "renameSuccess": "Perfil renombrado a '{{profile}}'", "enabledDescription": "Los perfiles están habilitados. Cree un nuevo perfil a continuación, navegue a una sección de configuración de cámara para realizar sus cambios y guarde para que estos surtan efecto.", - "disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda." + "disabledDescription": "Los perfiles le permiten definir conjuntos con nombre de anulaciones de configuración de la cámara (por ejemplo: armado, fuera, noche) que pueden activarse bajo demanda.", + "deleteProfile": "Eliminar perfil", + "deleteProfileConfirm": "¿Eliminar el perfil \"{{profile}}\" de todas las cámaras? Esta acción no se puede deshacer.", + "deleteSuccess": "Perfil '{{profile}}' eliminado", + "createSuccess": "Perfil '{{profile}}' creado", + "removeOverride": "Eliminar sobrescritura de perfil", + "deleteSection": "Eliminar sobrescrituras de sección", + "deleteSectionConfirm": "¿Eliminar las sobrescrituras de {{section}} del perfil {{profile}} en {{camera}}?", + "deleteSectionSuccess": "Sobrescrituras de {{section}} eliminadas para {{profile}}", + "enableSwitch": "Habilitar perfiles" }, "go2rtcStreams": { "renameStreamDesc": "Introduce un nuevo nombre para esta transmisión. Cambiar el nombre de una transmisión puede provocar fallos en las cámaras u otras transmisiones que hagan referencia a ella por su nombre.", "addStreamDesc": "Introduce un nombre para la nueva transmisión. Este nombre se utilizará para hacer referencia a la transmisión en la configuración de su cámara.", "description": "Gestione las configuraciones de transmisión de go2rtc para la retransmisión de cámaras. Cada transmisión tiene un nombre y una o más URL de origen.", - "deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar." + "deleteStreamConfirm": "¿Está seguro de que desea eliminar la transmisión \"{{streamName}}\"? Las cámaras que hagan referencia a esta transmisión podrían dejar de funcionar.", + "title": "Flujos go2rtc", + "addStream": "Añadir flujo", + "addUrl": "Añadir URL", + "streamName": "Nombre del flujo", + "streamNamePlaceholder": "p. ej., puerta_principal", + "streamUrlPlaceholder": "p. ej., rtsp://usuario:contraseña@192.168.1.100/stream", + "deleteStream": "Eliminar flujo", + "noStreams": "No hay flujos go2rtc configurados. Añade un flujo para empezar.", + "validation": { + "nameRequired": "El nombre del flujo es obligatorio", + "nameDuplicate": "Ya existe un flujo con este nombre", + "nameInvalid": "El nombre del flujo solo puede contener letras, números, guiones bajos y guiones", + "urlRequired": "Se requiere al menos una URL" + }, + "renameStream": "Renombrar flujo", + "newStreamName": "Nuevo nombre del flujo", + "ffmpeg": { + "useFfmpegModule": "Usar modo de compatibilidad (ffmpeg)", + "video": "Vídeo", + "audio": "Audio", + "hardware": "Aceleración por hardware", + "videoCopy": "Copiar", + "videoH264": "Transcodificar a H.264", + "videoH265": "Transcodificar a H.265", + "videoExclude": "Excluir", + "audioCopy": "Copiar", + "audioAac": "Transcodificar a AAC", + "audioOpus": "Transcodificar a Opus", + "audioPcmu": "Transcodificar a PCM μ-law", + "audioPcma": "Transcodificar a PCM A-law", + "audioPcm": "Transcodificar a PCM", + "audioMp3": "Transcodificar a MP3", + "audioExclude": "Excluir", + "hardwareNone": "Sin aceleración por hardware", + "hardwareAuto": "Automático (recomendado)", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "Añadir códec de vídeo", + "addAudioCodec": "Añadir códec de audio", + "removeCodec": "Eliminar códec" + }, + "streamNumber": "Flujo {{index}}" }, "configMessages": { "birdseye": { "objectsModeDetectDisabled": "Birdseye está configurado en modo 'objects', pero la detección de objetos está desactivada para esta cámara. La cámara no aparecerá en Birdseye." }, "lpr": { - "globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara." + "globalDisabled": "El reconocimiento de matrículas no está habilitado a nivel global. Habilítelo en la configuración global para que funcione el reconocimiento de matrículas a nivel de cámara.", + "vehicleNotTracked": "El reconocimiento de matrículas requiere rastrear 'car' o 'motorcycle'. Habilita 'car' o 'motorcycle' en Objetos para esta cámara.", + "modelSizeLarge": "El modelo 'large' está optimizado para matrículas de varias líneas. El modelo 'small' ofrece mejor rendimiento que 'large' y debería usarse salvo que tu región use formatos de matrícula de varias líneas." }, "audio": { "noAudioRole": "Ninguna transmisión tiene definido el rol de audio. Debe habilitar el rol de audio para que funcione la detección de audio." }, "faceRecognition": { - "personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos." + "personNotTracked": "El reconocimiento facial requiere que se realice el seguimiento del objeto 'person'. Asegúrese de que 'person' se encuentre en la lista de seguimiento de objetos.", + "globalDisabled": "El enriquecimiento de reconocimiento facial debe estar habilitado para que las funciones de reconocimiento facial funcionen en esta cámara.", + "modelSizeLarge": "El modelo 'large' requiere una GPU o NPU para ofrecer un rendimiento razonable. Usa 'small' en sistemas solo con CPU." }, "audioTranscription": { "audioDetectionDisabled": "La detección de audio no está habilitada para esta cámara. La transcripción de audio requiere que la detección de audio esté activa." @@ -1392,17 +1844,165 @@ "detectDisabled": "La detección de objetos está desactivada. Las instantáneas se generan a partir de los objetos rastreados y no se crearán." }, "detectors": { - "mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente." + "mixedTypes": "Todos los detectores deben ser del mismo tipo. Retire los detectores existentes para utilizar un tipo diferente.", + "mixedTypesSuggestion": "Todos los detectores deben usar el mismo tipo. Elimina los detectores existentes o selecciona {{type}}." }, "review": { - "detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones." + "detectDisabled": "La detección de objetos está desactivada. Los elementos de revisión requieren objetos detectados para categorizar las alertas y detecciones.", + "recordDisabled": "La grabación está deshabilitada; no se generarán elementos de revisión.", + "allNonAlertDetections": "Toda la actividad que no sea de alerta se incluirá como detecciones.", + "genaiImageSourceRecordingsRecordDisabled": "El origen de imagen está establecido en 'recordings', pero la grabación está deshabilitada. Frigate usará imágenes de vista previa como alternativa." + }, + "detect": { + "fpsGreaterThanFive": "No se recomienda establecer los FPS de detección por encima de 5. Valores más altos pueden causar problemas de rendimiento y no aportarán ningún beneficio.", + "disabled": "La detección de objetos está deshabilitada. Las instantáneas, los elementos de revisión y enriquecimientos como el reconocimiento facial, el reconocimiento de matrículas y la IA generativa no funcionarán." + }, + "objects": { + "genaiNoDescriptionsProvider": "Debes configurar un proveedor GenAI con el rol 'descriptions' para que se generen descripciones." + }, + "record": { + "noRecordRole": "Ningún flujo tiene definido el rol de grabación. La grabación no funcionará." + }, + "semanticSearch": { + "jinav2SmallModelSize": "El tamaño 'small' con el modelo Jina V2 tiene un alto consumo de RAM y coste de inferencia. Se recomienda el modelo 'large' con una GPU dedicada." } }, "resetToDefaultDescription": "Esto restablecerá todos los ajustes de esta sección a sus valores predeterminados. Esta acción no se puede deshacer.", "resetToGlobalDescription": "Esto restablecerá la configuración de esta sección a los valores predeterminados globales. Esta acción no se puede deshacer.", "detectionModel": { "plusActive": { - "description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+." + "description": "Esta instancia está ejecutando un modelo de Frigate+. Seleccione o cambie su modelo en la configuración de Frigate+.", + "title": "Gestión de modelos de Frigate+", + "label": "Origen del modelo actual", + "goToFrigatePlus": "Ir a los ajustes de Frigate+", + "showModelForm": "Configurar un modelo manualmente" } + }, + "saveAllPreview": { + "profile": { + "label": "Override,Eliminar" + }, + "title": "Cambios pendientes de guardar", + "triggerLabel": "Revisar cambios pendientes", + "empty": "No hay cambios pendientes.", + "scope": { + "label": "Ámbito", + "global": "Global", + "camera": "Cámara: {{cameraName}}" + }, + "field": { + "label": "Campo" + }, + "value": { + "label": "Nuevo valor", + "reset": "Restablecer" + } + }, + "timestampPosition": { + "tl": "Arriba a la izquierda", + "tr": "Arriba a la derecha", + "bl": "Abajo a la izquierda", + "br": "Abajo a la derecha" + }, + "unsavedChanges": "Tienes cambios sin guardar", + "confirmReset": "Confirmar restablecimiento", + "birdseye": { + "trackingMode": { + "objects": "Objetos", + "motion": "Movimiento", + "continuous": "Continuo" + }, + "cameraOrder": { + "label": "Orden de cámaras", + "description": "Arrastra las cámaras para establecer su orden en el diseño de Birdseye.", + "reorderHandle": "Arrastrar para reordenar", + "saving": "Guardando…", + "saved": "Guardado" + } + }, + "snapshot": { + "retainMode": { + "all": "Todo", + "motion": "Movimiento", + "active_objects": "Objetos activos" + } + }, + "ui": { + "timeFormat": { + "browser": "Navegador", + "12hour": "12 horas", + "24hour": "24 horas" + }, + "TimeOrDateStyle": { + "full": "Completo", + "long": "Largo", + "medium": "Medio", + "short": "Corto" + }, + "unitSystem": { + "metric": "Métrico", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Grabaciones", + "previews": "Vistas previas" + } + }, + "logger": { + "logLevel": { + "debug": "Depuración", + "info": "Información", + "warning": "Advertencia", + "error": "Error", + "critical": "Crítico" + } + }, + "modelSize": { + "small": "Pequeño", + "large": "Grande" + }, + "retainMode": { + "all": "Todo", + "motion": "Movimiento", + "active_objects": "Objetos activos" + }, + "previewQuality": { + "very_high": "Muy alto", + "high": "Alto", + "medium": "Medio", + "low": "Bajo", + "very_low": "Muy bajo" + }, + "detectorsAndModel": { + "title": "Detectores y modelo", + "description": "Configura el backend del detector que ejecuta la detección de objetos y el modelo que utiliza. Los cambios se guardan juntos para que el detector y el modelo permanezcan sincronizados.", + "cardTitles": { + "detector": "Hardware del detector", + "model": "Modelo de detección" + }, + "tabs": { + "plus": "Frigate+", + "custom": "Modelo personalizado" + }, + "mismatch": { + "warning": "El modelo actual de Frigate+ “{{model}}” requiere el detector {{required}}. Selecciona un modelo compatible a continuación o cambia a Modelo personalizado antes de guardar." + }, + "plusModel": { + "requiresDetector": "Requiere: {{detector}}", + "noModelSelected": "Selecciona un modelo de Frigate+" + }, + "toast": { + "saveSuccess": "Los ajustes de detectores y modelo se han guardado. Reinicia Frigate para aplicar los cambios.", + "saveError": "No se pudieron guardar los ajustes del detector y del modelo" + }, + "unsavedChanges": "Cambios sin guardar en el detector y el modelo", + "restartRequired": "Reinicio necesario (se ha cambiado el detector o el modelo)" + }, + "menuDot": { + "overrideGlobal": "Esta sección sobrescribe la configuración global", + "overrideProfile": "Esta sección está sobrescrita por el perfil {{profile}}", + "unsaved": "Esta sección tiene cambios sin guardar" } } diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json index e0a0157a1a..23ee553a12 100644 --- a/web/public/locales/es/views/system.json +++ b/web/public/locales/es/views/system.json @@ -55,7 +55,10 @@ }, "count_other": "{{count}} mensajes", "count_one": "{{count}} mensaje", - "empty": "No se han capturado mensaje aún" + "empty": "No se han capturado mensaje aún", + "expanded": { + "payload": "Carga útil" + } } }, "title": "Sistema", @@ -209,6 +212,9 @@ "unusable": "No usable", "fair": "Normal", "stallsLastHour": "Bloqueos (última hora)" + }, + "noCameras": { + "title": "No se han encontrado cámaras" } }, "lastRefreshed": "Última actualización: ", From 50f7f11f0b254d51e32465b173c96946966ac3a6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:16:58 +0200 Subject: [PATCH 23/94] Translated using Weblate (French) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (French) Currently translated at 46.6% (21 of 45 strings) Translated using Weblate (French) Currently translated at 30.0% (12 of 40 strings) Co-authored-by: Erwan Cogoluenhes Co-authored-by: Hosted Weblate Co-authored-by: Le Buzzy Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/fr/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/fr/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/fr/ Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-replay --- web/public/locales/fr/objects.json | 6 +++++- web/public/locales/fr/views/chat.json | 4 +++- web/public/locales/fr/views/replay.json | 27 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/web/public/locales/fr/objects.json b/web/public/locales/fr/objects.json index afc5791aea..fdd517c84c 100644 --- a/web/public/locales/fr/objects.json +++ b/web/public/locales/fr/objects.json @@ -121,5 +121,9 @@ "royal_mail": "Poste du Royaume Uni", "school_bus": "Bus scolaire", "skunk": "Mouffette", - "kangaroo": "Kangourou" + "kangaroo": "Kangourou", + "baby": "Bébé", + "baby_stroller": "Poussette", + "rickshaw": "Pousse-pousse", + "Rodent": "Rongeur" } diff --git a/web/public/locales/fr/views/chat.json b/web/public/locales/fr/views/chat.json index 5d75f17db7..72736b3d91 100644 --- a/web/public/locales/fr/views/chat.json +++ b/web/public/locales/fr/views/chat.json @@ -16,5 +16,7 @@ "attach_event_aria": "Attacher l'événement {{eventId}}", "attachment_picker_paste_label": "Ou coller l'event ID", "attachment_picker_placeholder": "Attacher un événement", - "quick_reply_find_similar": "Trouver des observations similaires" + "quick_reply_find_similar": "Trouver des observations similaires", + "no_similar_objects_found": "Aucun objet similaire trouvé.", + "semantic_search_required": "La recherche sémantique doit être activée afin de trouver un objet similaire." } diff --git a/web/public/locales/fr/views/replay.json b/web/public/locales/fr/views/replay.json index e230ad3f7c..abafbe755f 100644 --- a/web/public/locales/fr/views/replay.json +++ b/web/public/locales/fr/views/replay.json @@ -3,6 +3,31 @@ "description": "Rejouer les enregistrement de la camera, à but de débogage. La liste d'objets montre un résumé avec retard des objets détectés; et l'onglet Messages montre le flux des messages internes à Frigate liés à la vidéo rejouée.", "websocket_messages": "Messages", "dialog": { - "title": "Démarrer le Rejeu-Debogage" + "title": "Démarrer le Rejeu-Debogage", + "timeRange": "Intervalle", + "preset": { + "1m": "Dernière minute", + "5m": "5 dernières minutes", + "timeline": "Depuis la chronologie", + "custom": "Personnalisé" + }, + "startButton": "Démarrer le revisionnage", + "selectFromTimeline": "Sélectionner", + "starting": "Démarrage du revisionnage...", + "startLabel": "Démarrer", + "endLabel": "Fin", + "toast": { + "error": "Echec du démarrage du revisionnage de déboggage : {{error}}", + "alreadyActive": "Une session de revisionnage est déjà active", + "stopError": "Echec de l'arrêt du revisionnage de déboggage : {{error}}", + "goToReplay": "Vers le revisionnage" + } + }, + "page": { + "noSession": "Aucune session de revisionnage de déboggage active", + "noSessionDesc": "Démarrer un revisionnage de déboggage depuis l'Historique en cliquant sur le boutons Actions dans la barre d'outils et choisir Revisionnage de déboggage.", + "goToRecordings": "Vers l'historique", + "preparingClip": "Préparation du clip…", + "preparingClipDesc": "Frigate est encore en train de recoller les enregistrements pour l'intervalle de temps sélectionnée. Cela peut prendre une minute pour les plus longues intervalles." } } From fac11286f59f2a6760ed2688b13e85209d9fb26b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:17:00 +0200 Subject: [PATCH 24/94] Translated using Weblate (Urdu) Currently translated at 4.0% (1 of 25 strings) Translated using Weblate (Urdu) Currently translated at 4.5% (1 of 22 strings) Translated using Weblate (Urdu) Currently translated at 0.1% (1 of 794 strings) Translated using Weblate (Urdu) Currently translated at 0.2% (1 of 473 strings) Co-authored-by: Hosted Weblate Co-authored-by: Muhammad Arsalan Siddiqui Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ur/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ur/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ur/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ur/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation --- web/public/locales/ur/config/cameras.json | 4 +++- web/public/locales/ur/config/global.json | 6 +++++- web/public/locales/ur/config/groups.json | 8 +++++++- web/public/locales/ur/config/validation.json | 4 +++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/web/public/locales/ur/config/cameras.json b/web/public/locales/ur/config/cameras.json index 0967ef424b..23c240d85a 100644 --- a/web/public/locales/ur/config/cameras.json +++ b/web/public/locales/ur/config/cameras.json @@ -1 +1,3 @@ -{} +{ + "label": "کیمرے کی ترتیب" +} diff --git a/web/public/locales/ur/config/global.json b/web/public/locales/ur/config/global.json index 0967ef424b..805c0d3b7d 100644 --- a/web/public/locales/ur/config/global.json +++ b/web/public/locales/ur/config/global.json @@ -1 +1,5 @@ -{} +{ + "version": { + "label": "موجودہ کنفیگریشن ورژن" + } +} diff --git a/web/public/locales/ur/config/groups.json b/web/public/locales/ur/config/groups.json index 0967ef424b..13c20a36ec 100644 --- a/web/public/locales/ur/config/groups.json +++ b/web/public/locales/ur/config/groups.json @@ -1 +1,7 @@ -{} +{ + "audio": { + "global": { + "detection": "عالمی کھوج" + } + } +} diff --git a/web/public/locales/ur/config/validation.json b/web/public/locales/ur/config/validation.json index 0967ef424b..b871b9f6ed 100644 --- a/web/public/locales/ur/config/validation.json +++ b/web/public/locales/ur/config/validation.json @@ -1 +1,3 @@ -{} +{ + "minimum": "کم از کم {{limit}} ہونا چاہیے" +} From b470258d959223bdd39975b2b2094d9648077d3f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:17:01 +0200 Subject: [PATCH 25/94] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (101 of 101 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (64 of 64 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (175 of 175 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (47 of 47 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (45 of 45 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (86 of 86 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (145 of 145 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (25 of 25 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (59 of 59 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (100 of 100 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (501 of 501 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (129 of 129 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (26 of 26 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (22 of 22 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (1150 of 1150 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (473 of 473 strings) Co-authored-by: Hosted Weblate Co-authored-by: Jamie HUANG <114514020@live.asia.edu.tw> Co-authored-by: fascinate722 Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hant/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/Config - Groups Translation: Frigate NVR/Config - Validation Translation: Frigate NVR/audio Translation: Frigate NVR/common Translation: Frigate NVR/components-camera Translation: Frigate NVR/components-dialog Translation: Frigate NVR/components-player Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-classificationmodel Translation: Frigate NVR/views-events Translation: Frigate NVR/views-explore Translation: Frigate NVR/views-exports Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/zh-Hant/audio.json | 424 ++++- web/public/locales/zh-Hant/common.json | 37 +- .../locales/zh-Hant/components/camera.json | 3 +- .../locales/zh-Hant/components/dialog.json | 64 +- .../locales/zh-Hant/components/player.json | 3 +- .../locales/zh-Hant/config/cameras.json | 918 +++++++++ web/public/locales/zh-Hant/config/global.json | 1582 +++++++++++++++- web/public/locales/zh-Hant/config/groups.json | 74 +- .../locales/zh-Hant/config/validation.json | 33 +- web/public/locales/zh-Hant/objects.json | 11 +- web/public/locales/zh-Hant/views/chat.json | 65 +- .../zh-Hant/views/classificationModel.json | 88 +- web/public/locales/zh-Hant/views/events.json | 33 +- web/public/locales/zh-Hant/views/explore.json | 21 +- web/public/locales/zh-Hant/views/exports.json | 111 +- .../locales/zh-Hant/views/faceLibrary.json | 19 +- web/public/locales/zh-Hant/views/live.json | 35 +- .../locales/zh-Hant/views/motionSearch.json | 74 +- web/public/locales/zh-Hant/views/replay.json | 60 +- .../locales/zh-Hant/views/settings.json | 1641 ++++++++++++++++- web/public/locales/zh-Hant/views/system.json | 61 +- 21 files changed, 5299 insertions(+), 58 deletions(-) diff --git a/web/public/locales/zh-Hant/audio.json b/web/public/locales/zh-Hant/audio.json index 9a458ce9c3..f5dd289f88 100644 --- a/web/public/locales/zh-Hant/audio.json +++ b/web/public/locales/zh-Hant/audio.json @@ -77,5 +77,427 @@ "chatter": "嘈雜聲", "crowd": "人群聲", "children_playing": "兒童嬉鬧聲", - "pets": "寵物" + "pets": "寵物", + "yip": "吠叫", + "howl": "嚎叫", + "bow_wow": "汪汪", + "growling": "咆哮", + "whimper_dog": "狗嗚咽", + "purr": "咕嚕", + "meow": "喵喵", + "hiss": "嘶嘶聲", + "caterwaul": "貓叫春", + "livestock": "牲畜", + "clip_clop": "蹄聲", + "neigh": "嘶鳴", + "cattle": "牛", + "moo": "哞哞", + "cowbell": "牛鈴", + "pig": "豬", + "oink": "哼哼", + "bleat": "咩咩", + "fowl": "家禽", + "chicken": "雞", + "cluck": "咯咯", + "cock_a_doodle_doo": "喔喔", + "turkey": "火雞", + "gobble": "咯咯", + "duck": "鴨子", + "quack": "嘎嘎", + "goose": "鵝", + "honk": "鳴笛/鵝叫聲", + "wild_animals": "野生動物", + "roaring_cats": "吼叫的貓科動物", + "roar": "吼叫", + "chirp": "啾啾", + "squawk": "啼叫", + "pigeon": "鴿子", + "coo": "咕咕", + "crow": "烏鴉", + "caw": "呱呱", + "owl": "貓頭鷹", + "hoot": "嗚嗚", + "flapping_wings": "翅膀拍打", + "dogs": "狗群", + "rats": "老鼠", + "patter": "啪嗒聲", + "insect": "昆蟲", + "cricket": "蟋蟀", + "mosquito": "蚊子", + "fly": "蒼蠅", + "buzz": "嗡嗡", + "frog": "青蛙", + "croak": "呱呱", + "snake": "蛇", + "rattle": "響尾", + "whale_vocalization": "鯨魚叫聲", + "music": "音樂", + "musical_instrument": "樂器", + "plucked_string_instrument": "彈撥樂器", + "guitar": "吉他", + "electric_guitar": "電吉他", + "bass_guitar": "貝斯", + "acoustic_guitar": "原聲吉他", + "steel_guitar": "鋼弦吉他", + "tapping": "敲擊", + "strum": "掃弦", + "banjo": "班卓琴", + "sitar": "西塔琴", + "mandolin": "曼陀林", + "zither": "古箏", + "ukulele": "尤克里裡", + "piano": "鋼琴", + "electric_piano": "電鋼琴", + "organ": "風琴", + "electronic_organ": "電子琴", + "hammond_organ": "哈蒙德風琴", + "synthesizer": "合成器", + "sampler": "取樣器", + "harpsichord": "大鍵琴", + "percussion": "打擊樂器", + "drum_kit": "架子鼓", + "drum_machine": "鼓機", + "drum": "鼓", + "snare_drum": "軍鼓", + "rimshot": "鼓邊擊", + "drum_roll": "滾鼓", + "bass_drum": "大鼓", + "timpani": "定音鼓", + "tabla": "塔布拉鼓", + "cymbal": "鈸", + "hi_hat": "踩鑔", + "wood_block": "木魚", + "tambourine": "鈴鼓", + "maraca": "沙錘", + "gong": "鑼", + "tubular_bells": "管鍾", + "mallet_percussion": "槌擊打擊樂器", + "marimba": "馬林巴", + "glockenspiel": "鐘琴", + "vibraphone": "顫音琴", + "steelpan": "鋼鼓", + "orchestra": "管絃樂隊", + "brass_instrument": "銅管樂器", + "french_horn": "圓號", + "trumpet": "小號", + "trombone": "長號", + "bowed_string_instrument": "弓弦樂器", + "string_section": "絃樂組", + "violin": "小提琴", + "pizzicato": "撥絃", + "cello": "大提琴", + "double_bass": "低音提琴", + "wind_instrument": "管樂器", + "flute": "長笛", + "saxophone": "薩克斯", + "clarinet": "單簧管", + "harp": "豎琴", + "bell": "鈴", + "church_bell": "教堂鍾", + "jingle_bell": "鈴鐺", + "bicycle_bell": "腳踏車鈴", + "tuning_fork": "音叉", + "chime": "風鈴", + "wind_chime": "風鈴", + "harmonica": "口琴", + "accordion": "手風琴", + "bagpipes": "風笛", + "didgeridoo": "迪吉里杜管", + "theremin": "特雷門琴", + "singing_bowl": "頌缽", + "scratching": "刮擦聲", + "pop_music": "流行音樂", + "hip_hop_music": "嘻哈音樂", + "beatboxing": "人聲節拍", + "rock_music": "搖滾音樂", + "heavy_metal": "重金屬", + "punk_rock": "朋克搖滾", + "grunge": "垃圾搖滾", + "progressive_rock": "前衛搖滾", + "rock_and_roll": "搖滾樂", + "psychedelic_rock": "迷幻搖滾", + "rhythm_and_blues": "節奏布魯斯", + "soul_music": "靈魂樂", + "reggae": "雷鬼", + "country": "鄉村音樂", + "swing_music": "搖擺樂", + "bluegrass": "藍草音樂", + "funk": "放克", + "folk_music": "民謠", + "middle_eastern_music": "中東音樂", + "jazz": "爵士樂", + "disco": "迪斯科", + "classical_music": "古典音樂", + "opera": "歌劇", + "electronic_music": "電子音樂", + "house_music": "浩室音樂", + "techno": "科技舞曲", + "dubstep": "迴響貝斯", + "drum_and_bass": "鼓打貝斯", + "electronica": "電子樂", + "electronic_dance_music": "電子舞曲", + "ambient_music": "環境音樂", + "trance_music": "迷幻舞曲", + "music_of_latin_america": "拉丁美洲音樂", + "salsa_music": "薩爾薩", + "flamenco": "弗拉門戈", + "blues": "藍調", + "music_for_children": "兒童音樂", + "new-age_music": "新世紀音樂", + "vocal_music": "聲樂", + "a_capella": "無伴奏合唱", + "music_of_africa": "非洲音樂", + "afrobeat": "非洲節拍", + "christian_music": "基督教音樂", + "gospel_music": "福音音樂", + "music_of_asia": "亞洲音樂", + "carnatic_music": "卡納提克音樂", + "music_of_bollywood": "寶萊塢音樂", + "ska": "斯卡", + "traditional_music": "傳統音樂", + "independent_music": "獨立音樂", + "song": "歌曲", + "background_music": "背景音樂", + "theme_music": "主題音樂", + "jingle": "廣告歌", + "soundtrack_music": "配樂", + "lullaby": "搖籃曲", + "video_game_music": "電子遊戲音樂", + "christmas_music": "聖誕音樂", + "dance_music": "舞曲", + "wedding_music": "婚禮音樂", + "happy_music": "歡快音樂", + "sad_music": "悲傷音樂", + "tender_music": "溫柔音樂", + "exciting_music": "激動音樂", + "angry_music": "憤怒音樂", + "scary_music": "恐怖音樂", + "wind": "風", + "rustling_leaves": "樹葉沙沙聲", + "wind_noise": "風聲", + "thunderstorm": "雷暴", + "thunder": "雷聲", + "water": "水", + "rain": "雨", + "raindrop": "雨滴", + "rain_on_surface": "雨打表面", + "stream": "溪流", + "waterfall": "瀑布", + "ocean": "海洋", + "waves": "波浪", + "steam": "蒸汽", + "gurgling": "汩汩聲", + "fire": "火", + "crackle": "噼啪聲", + "sailboat": "帆船", + "rowboat": "划艇", + "motorboat": "摩托艇", + "ship": "輪船", + "motor_vehicle": "機動車", + "toot": "鳴笛", + "car_alarm": "汽車警報", + "power_windows": "電動車窗", + "skidding": "輪胎打滑", + "tire_squeal": "輪胎尖叫", + "car_passing_by": "汽車駛過", + "race_car": "賽車", + "truck": "卡車", + "air_brake": "氣閘", + "air_horn": "氣笛", + "reversing_beeps": "倒車提示音", + "ice_cream_truck": "冰淇淋車", + "emergency_vehicle": "應急車輛", + "police_car": "警車", + "ambulance": "救護車", + "fire_engine": "消防車", + "traffic_noise": "交通噪音", + "rail_transport": "鐵路運輸", + "train_whistle": "火車汽笛", + "train_horn": "火車鳴笛", + "railroad_car": "鐵路車廂", + "train_wheels_squealing": "火車輪子尖叫", + "subway": "地鐵", + "aircraft": "飛行器", + "aircraft_engine": "飛機引擎", + "jet_engine": "噴氣引擎", + "propeller": "螺旋槳", + "helicopter": "直升機", + "fixed-wing_aircraft": "固定翼飛機", + "engine": "引擎", + "light_engine": "輕型引擎", + "dental_drill's_drill": "牙科鑽", + "lawn_mower": "割草機", + "chainsaw": "電鋸", + "medium_engine": "中型引擎", + "heavy_engine": "重型引擎", + "engine_knocking": "引擎敲擊", + "engine_starting": "引擎啟動", + "idling": "怠速", + "accelerating": "加速", + "doorbell": "門鈴", + "ding-dong": "叮咚", + "sliding_door": "滑動門", + "slam": "猛關", + "knock": "敲門", + "tap": "輕敲", + "squeak": "吱吱聲", + "cupboard_open_or_close": "櫥櫃開關", + "drawer_open_or_close": "抽屜開關", + "dishes": "餐具", + "cutlery": "刀叉", + "chopping": "切菜", + "frying": "煎炸", + "microwave_oven": "微波爐", + "water_tap": "水龍頭", + "bathtub": "浴缸", + "toilet_flush": "馬桶沖水", + "electric_toothbrush": "電動牙刷", + "vacuum_cleaner": "吸塵器", + "zipper": "拉鍊", + "keys_jangling": "鑰匙叮噹", + "coin": "硬幣", + "electric_shaver": "電動剃鬚刀", + "shuffling_cards": "洗牌", + "typing": "打字", + "typewriter": "打字機", + "computer_keyboard": "電腦鍵盤", + "writing": "書寫", + "alarm": "警報", + "telephone": "電話", + "telephone_bell_ringing": "電話鈴聲", + "ringtone": "手機鈴聲", + "telephone_dialing": "電話撥號", + "dial_tone": "撥號音", + "busy_signal": "忙音", + "alarm_clock": "鬧鐘", + "siren": "警笛", + "civil_defense_siren": "防空警報", + "buzzer": "蜂鳴器", + "smoke_detector": "煙霧檢測器", + "fire_alarm": "火災警報器", + "foghorn": "霧笛", + "whistle": "哨子", + "steam_whistle": "蒸汽汽笛", + "mechanisms": "機械裝置", + "ratchet": "棘輪", + "tick": "滴答", + "tick-tock": "滴答滴答", + "gears": "齒輪", + "pulleys": "滑輪", + "sewing_machine": "縫紉機", + "mechanical_fan": "機械風扇", + "air_conditioning": "空調", + "cash_register": "收銀機", + "printer": "印表機", + "single-lens_reflex_camera": "單反相機", + "tools": "工具", + "hammer": "錘子", + "jackhammer": "風鎬", + "sawing": "鋸", + "filing": "銼", + "sanding": "砂磨", + "power_tool": "電動工具", + "drill": "電鑽", + "explosion": "爆炸", + "gunshot": "槍聲", + "machine_gun": "機關槍", + "fusillade": "齊射", + "artillery_fire": "炮火", + "cap_gun": "玩具槍", + "fireworks": "煙花", + "firecracker": "鞭炮", + "burst": "爆裂", + "eruption": "爆發", + "boom": "轟隆", + "wood": "木頭", + "chop": "砍", + "splinter": "碎裂", + "crack": "破裂", + "glass": "玻璃", + "chink": "叮噹", + "shatter": "粉碎", + "silence": "寂靜", + "sound_effect": "音效", + "environmental_noise": "環境噪音", + "static": "靜電噪音", + "white_noise": "白噪音", + "pink_noise": "粉紅噪音", + "television": "電視", + "radio": "收音機", + "field_recording": "實地錄音", + "scream": "尖叫", + "sodeling": "索德鈴", + "chird": "啾鳴", + "change_ringing": "變奏鐘聲", + "shofar": "羊角號", + "liquid": "液體", + "splash": "液體飛濺", + "slosh": "液體晃動", + "squish": "擠壓", + "drip": "水滴聲", + "pour": "倒水聲", + "trickle": "細流水聲", + "gush": "液體噴湧", + "fill": "注水聲", + "spray": "噴灑", + "pump": "泵送", + "stir": "攪拌聲", + "boiling": "沸騰聲", + "sonar": "聲吶聲", + "arrow": "箭矢聲", + "whoosh": "呼嘯聲", + "thump": "砰擊聲", + "thunk": "沉悶聲", + "electronic_tuner": "電子調音器", + "effects_unit": "效果器", + "chorus_effect": "合唱效果", + "basketball_bounce": "籃球反彈聲", + "bang": "砰聲", + "slap": "拍擊聲", + "whack": "重擊聲", + "smash": "猛擊聲", + "breaking": "破碎聲", + "bouncing": "彈跳聲", + "whip": "鞭打聲", + "flap": "撲動聲", + "scratch": "刮擦聲", + "scrape": "刮擦聲", + "rub": "摩擦聲", + "roll": "捲動聲", + "crushing": "壓碎聲", + "crumpling": "揉皺聲", + "tearing": "撕裂聲", + "beep": "嗶聲", + "ping": "嘀聲", + "ding": "叮聲", + "clang": "鐺聲", + "squeal": "尖銳聲", + "creak": "嘎吱聲", + "rustle": "沙沙聲", + "whir": "嗡聲", + "clatter": "哐啷聲", + "sizzle": "滋滋聲", + "clicking": "點選聲", + "clickety_clack": "咔嗒聲", + "rumble": "隆隆聲", + "plop": "撲通聲", + "hum": "嗡鳴聲", + "zing": "嗖聲", + "boing": "嘣聲", + "crunch": "咔嚓聲", + "sine_wave": "正弦波聲", + "harmonic": "諧波聲", + "chirp_tone": "啾聲", + "pulse": "脈衝", + "inside": "室內聲", + "outside": "室外聲", + "reverberation": "混響", + "echo": "回聲", + "noise": "噪聲", + "mains_hum": "電流嗡聲", + "distortion": "失真聲", + "sidetone": "旁音", + "cacophony": "刺耳噪聲", + "throbbing": "脈動聲", + "vibration": "振動聲" } diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json index 17a60efaa6..e6e358bfb3 100644 --- a/web/public/locales/zh-Hant/common.json +++ b/web/public/locales/zh-Hant/common.json @@ -69,7 +69,8 @@ }, "inProgress": "處理中", "invalidStartTime": "無效的起始時間", - "invalidEndTime": "無效的結束時間" + "invalidEndTime": "無效的結束時間", + "never": "從不" }, "unit": { "speed": { @@ -95,7 +96,8 @@ "show": "顯示{{item}}", "ID": "ID", "none": "無", - "all": "全部" + "all": "全部", + "other": "其他" }, "button": { "apply": "套用", @@ -133,7 +135,19 @@ "export": "匯出", "deleteNow": "立即刪除", "next": "繼續", - "continue": "繼續" + "continue": "繼續", + "add": "新增", + "applying": "應用中…", + "undo": "撤銷", + "copiedToClipboard": "已複製到剪貼簿", + "modified": "已修改", + "overridden": "已覆蓋", + "resetToGlobal": "重設為全域性", + "resetToDefault": "重設為預設", + "saveAll": "儲存全部", + "savingAll": "儲存全部中…", + "undoAll": "撤銷全部", + "retry": "重試" }, "menu": { "system": "系統", @@ -185,7 +199,9 @@ "bg": "Български (保加利亞文)", "gl": "Galego (加利西亞文)", "id": "Bahasa Indonesia (印尼文)", - "ur": "اردو (烏爾都文)" + "ur": "اردو (烏爾都文)", + "hr": "Hrvatski(克羅地亞語)", + "bs": "Bosanski (波士尼亞語)" }, "appearance": "外觀", "darkMode": { @@ -233,7 +249,11 @@ "logout": "登出", "setPassword": "設定密碼" }, - "classification": "標籤分類" + "classification": "標籤分類", + "profiles": "設定檔", + "actions": "操作", + "features": "功能", + "chat": "聊天" }, "toast": { "copyUrlToClipboard": "已複製連結至剪貼簿。", @@ -242,7 +262,8 @@ "error": { "title": "保存設定變更失敗:{{errorMessage}}", "noMessage": "保存設定變更失敗" - } + }, + "success": "成功儲存設定檔。" } }, "role": { @@ -286,5 +307,7 @@ }, "information": { "pixels": "{{area}}px" - } + }, + "no_items": "沒有項目", + "validation_errors": "驗證錯誤" } diff --git a/web/public/locales/zh-Hant/components/camera.json b/web/public/locales/zh-Hant/components/camera.json index 3bace4d9de..1676e0da75 100644 --- a/web/public/locales/zh-Hant/components/camera.json +++ b/web/public/locales/zh-Hant/components/camera.json @@ -82,6 +82,7 @@ "zones": "區域", "mask": "遮罩", "motion": "移動", - "regions": "區塊" + "regions": "區塊", + "paths": "行動軌跡" } } diff --git a/web/public/locales/zh-Hant/components/dialog.json b/web/public/locales/zh-Hant/components/dialog.json index b28ccca480..3d6f33a684 100644 --- a/web/public/locales/zh-Hant/components/dialog.json +++ b/web/public/locales/zh-Hant/components/dialog.json @@ -6,7 +6,8 @@ "title": "Frigate 正在重新啟動", "content": "此頁面將在 {{countdown}} 秒後重新載入。", "button": "立即重新載入" - } + }, + "description": "Frigate 在重啟期間將短暫停止執行。" }, "explore": { "plus": { @@ -57,11 +58,60 @@ "endTimeMustAfterStartTime": "結束時間必須要在開始時間之後", "noVaildTimeSelected": "沒有選取有效的時間範圍" }, - "view": "查看" + "view": "查看", + "queued": "匯出已加入佇列。請在匯出頁面檢視進度。", + "batchSuccess_other": "已開始 {{count}} 個匯出,正在開啟案件。", + "batchPartial": "已開始 {{total}} 個匯出中的 {{successful}} 個。失敗的攝影機:{{failedCameras}}", + "batchFailed": "啟動匯出失敗(共 {{total}} 個)。失敗的攝影機:{{failedCameras}}", + "batchQueuedSuccess_other": "已排隊 {{count}} 個匯出,正在開啟案件。", + "batchQueuedPartial": "已將 {{total}} 個匯出中的 {{successful}} 個加入佇列。失敗的攝影機:{{failedCameras}}", + "batchQueueFailed": "未能將 {{total}} 個匯出加入佇列。失敗的攝影機:{{failedCameras}}" }, "fromTimeline": { "saveExport": "保存匯出資料", - "previewExport": "預覽匯出資料" + "previewExport": "預覽匯出資料", + "queueingExport": "正在加入匯出佇列…", + "useThisRange": "使用此範圍" + }, + "case": { + "newCaseOption": "建立新案件", + "newCaseNamePlaceholder": "新案件名稱", + "newCaseDescriptionPlaceholder": "案件描述", + "label": "案件", + "nonAdminHelp": "將為這些匯出檔案建立一個新的案件。", + "placeholder": "選擇案件" + }, + "queueing": "正在加入匯出佇列…", + "tabs": { + "export": "單個攝影機", + "multiCamera": "多個攝影機" + }, + "multiCamera": { + "timeRange": "時間範圍", + "selectFromTimeline": "從時間線選擇", + "cameraSelection": "攝影機", + "cameraSelectionHelp": "在此時間範圍內具有追蹤目標的攝影機會被預先選中", + "checkingActivity": "正在檢查攝影機活動…", + "noCameras": "沒有可用的攝影機", + "detectionCount_other": "{{count}} 個追蹤目標", + "nameLabel": "匯出名稱", + "namePlaceholder": "這些匯出檔案的可選基礎名稱", + "queueingButton": "正在加入匯出佇列…", + "exportButton_other": "匯出 {{count}} 個攝影機" + }, + "multi": { + "title_other": "匯出 {{count}} 個審閱", + "description": "匯出每個選定的審閱項。所有匯出檔案將歸入同一個案件。", + "descriptionNoCase": "匯出每個選定的審閱項。", + "caseNamePlaceholder": "審閱匯出 - {{date}}", + "exportButton_other": "匯出 {{count}} 個審閱", + "exportingButton": "匯出中…", + "toast": { + "started_other": "已開始 {{count}} 個匯出。正在開啟案件。", + "startedNoCase_other": "已開始 {{count}} 個匯出。", + "partial": "已啟動 {{total}} 個匯出,其中 {{successful}} 個成功。失敗項:{{failedItems}}", + "failed": "啟動匯出失敗(共 {{total}} 個)。失敗項:{{failedItems}}" + } } }, "streaming": { @@ -109,6 +159,14 @@ "markAsReviewed": "標記為已審核", "deleteNow": "立即刪除", "markAsUnreviewed": "標記為未審核" + }, + "shareTimestamp": { + "label": "分享該時間片段", + "title": "分享該時間片段", + "description": "分享帶當前錄製播放時間的網址,或選擇自訂時間。請注意這不是公開的分享連結,只有具備 Frigate 及此攝影機存取權限的使用者才能存取。", + "custom": "自訂時間", + "button": "分享時間片段網址", + "shareTitle": "Frigate 審閱時間:{{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/zh-Hant/components/player.json b/web/public/locales/zh-Hant/components/player.json index dbecdb2beb..17015bd483 100644 --- a/web/public/locales/zh-Hant/components/player.json +++ b/web/public/locales/zh-Hant/components/player.json @@ -4,7 +4,8 @@ "noPreviewFoundFor": "找不到 {{cameraName}} 的預覽", "submitFrigatePlus": { "title": "提交此畫面至 Frigate+?", - "submit": "提交" + "submit": "提交", + "previewError": "無法載入快照預覽。該錄製當前可能不可用。" }, "streamOffline": { "desc": "{{cameraName}} 的 detect 串流未接收到任何畫面,請檢查錯誤日誌", diff --git a/web/public/locales/zh-Hant/config/cameras.json b/web/public/locales/zh-Hant/config/cameras.json index 8602044aa0..d2fd49f599 100644 --- a/web/public/locales/zh-Hant/config/cameras.json +++ b/web/public/locales/zh-Hant/config/cameras.json @@ -30,6 +30,924 @@ "listen": { "label": "監聽的音訊類型", "description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "filters": { + "label": "音訊過濾器", + "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。", + "threshold": { + "label": "最低音訊置信度", + "description": "音訊事件被計入的最低置信度閾值。" + } + }, + "enabled_in_config": { + "label": "原始音訊狀態", + "description": "指示原始靜態設定檔中是否開啟了音訊偵測。" + }, + "num_threads": { + "label": "偵測執行緒", + "description": "用於音訊偵測處理的執行緒數量。" } + }, + "mqtt": { + "label": "MQTT", + "description": "MQTT 影像釋出設定。", + "enabled": { + "label": "傳送影像", + "description": "為此攝影機啟用向 MQTT 主題釋出目標影像快照。" + }, + "timestamp": { + "label": "新增時間戳", + "description": "在釋出到 MQTT 的影像上疊加時間戳。" + }, + "bounding_box": { + "label": "新增邊界框", + "description": "在透過 MQTT 釋出的影像上繪製邊界框。" + }, + "crop": { + "label": "裁剪影像", + "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。" + }, + "height": { + "label": "影像高度", + "description": "透過 MQTT 釋出的影像的調整高度(像素)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能釋出 MQTT 影像的區域。" + }, + "quality": { + "label": "JPEG 品質", + "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。" + } + }, + "notifications": { + "label": "通知", + "enabled": { + "label": "開啟通知", + "description": "為此攝影機啟用或停用通知。" + }, + "email": { + "label": "通知郵箱", + "description": "用於推送通知或某些通知提供商要求的郵箱地址。" + }, + "cooldown": { + "label": "冷卻時間", + "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。" + }, + "enabled_in_config": { + "label": "原始通知狀態", + "description": "指示原始靜態配置中是否啟用了通知。" + }, + "description": "為此攝影機啟用和控制通知的設定。" + }, + "birdseye": { + "label": "鳥瞰圖", + "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。", + "enabled": { + "label": "開啟鳥瞰圖", + "description": "開啟或關閉鳥瞰圖功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。" + }, + "order": { + "label": "排序位置", + "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。" + } + }, + "detect": { + "label": "目標偵測", + "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。", + "enabled": { + "label": "開啟目標偵測", + "description": "開啟或關閉該攝影機的目標偵測。" + }, + "height": { + "label": "偵測畫面高度", + "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。" + }, + "width": { + "label": "偵測畫面寬度", + "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。" + }, + "fps": { + "label": "偵測幀率", + "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。" + }, + "min_initialized": { + "label": "最小初始化幀數", + "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。" + }, + "stationary": { + "label": "靜止目標配置", + "description": "用於偵測和管理長時間靜止目標的相關設定。", + "interval": { + "label": "靜止間隔", + "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。" + }, + "threshold": { + "label": "靜止閾值", + "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。", + "default": { + "label": "預設最大幀數", + "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。" + }, + "objects": { + "label": "目標最大幀數", + "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。" + } + }, + "classifier": { + "label": "開啟視覺分類器", + "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。" + } + }, + "annotation_offset": { + "label": "標記偏移量", + "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。", + "path": { + "label": "FFmpeg 路徑", + "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "傳遞給 FFmpeg 程序的全域性引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。" + }, + "input_args": { + "label": "輸入引數", + "description": "應用於 FFmpeg 輸入影片流的輸入引數。" + }, + "output_args": { + "label": "輸出引數", + "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。", + "detect": { + "label": "偵測輸出引數", + "description": "偵測功能影片流的預設輸出引數。" + }, + "record": { + "label": "錄製輸出引數", + "description": "錄製功能影片流的預設輸出引數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 相容性", + "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在啟用硬體加速時,預設使用的 GPU 索引。" + }, + "inputs": { + "label": "攝影機輸入影片流", + "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。", + "path": { + "label": "輸入路徑", + "description": "攝影機輸入影片流的地址或路徑。" + }, + "roles": { + "label": "輸入流功能", + "description": "定義該影片流的功能。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "該輸入影片流的硬體加速引數。" + }, + "input_args": { + "label": "輸入引數", + "description": "該影片流特定的輸入引數。" + } + } + }, + "live": { + "label": "即時監控觀看", + "streams": { + "label": "即時監控流名稱", + "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。" + }, + "height": { + "label": "即時監控高度", + "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。" + }, + "quality": { + "label": "即時監控品質", + "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。" + }, + "description": "用於控制即時流選擇、解析度和品質的網頁頁面設定。" + }, + "motion": { + "label": "畫面變動偵測", + "enabled": { + "label": "開啟畫面變動偵測", + "description": "開啟或關閉此攝影機的畫面變動偵測。" + }, + "threshold": { + "label": "畫面變動閾值", + "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃電閾值", + "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。" + }, + "skip_motion_threshold": { + "label": "跳過畫面變動閾值", + "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。" + }, + "improve_contrast": { + "label": "改善對比度", + "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。" + }, + "contour_area": { + "label": "輪廓區域", + "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "畫面 alpha 通道", + "description": "畫面變動預處理時混合畫面所使用的 alpha 值。" + }, + "frame_height": { + "label": "畫面高度", + "description": "計算畫面變動時縮放畫面的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。" + }, + "enabled_in_config": { + "label": "原始畫面變動狀態", + "description": "指示原始靜態配置中是否啟用了畫面變動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "description": "此攝影機的預設畫面變動偵測設定。" + }, + "objects": { + "label": "目標", + "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。", + "track": { + "label": "要追蹤的目標", + "description": "此攝影機要追蹤的目標標籤清單。" + }, + "filters": { + "label": "目標過濾器", + "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目標遮罩", + "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目標配置", + "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。", + "enabled": { + "label": "開啟生成式 AI", + "description": "預設開啟生成式 AI 生成追蹤目標的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "目標提示", + "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。" + }, + "objects": { + "label": "生成式 AI 目標", + "description": "預設傳送給生成式 AI 的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。" + }, + "send_triggers": { + "label": "生成式 AI 觸發器", + "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "目標追蹤結束時向生成式 AI 傳送請求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前觸發", + "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "表示在原始靜態配置中是否已啟用生成式 AI。" + } + } + }, + "record": { + "label": "錄影", + "enabled": { + "label": "開啟錄影", + "description": "開啟或關閉此攝影機的錄影。" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔分鐘數。" + }, + "continuous": { + "label": "持續保留", + "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "motion": { + "label": "動作保留", + "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "detections": { + "label": "偵測保留", + "description": "偵測事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "alerts": { + "label": "警報保留", + "description": "警報事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "export": { + "label": "匯出配置", + "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。", + "hwaccel_args": { + "label": "匯出硬體加速引數", + "description": "用於匯出/轉碼操作的硬體加速引數。" + }, + "max_concurrent": { + "label": "最大併發匯出數", + "description": "同時可處理的最大匯出任務數量。" + } + }, + "preview": { + "label": "預覽配置", + "description": "控制介面中顯示的錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質級別(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "指示原始靜態配置中是否啟用了錄影。" + }, + "description": "此攝影機的錄影和保留設定。" + }, + "review": { + "label": "審閱", + "alerts": { + "label": "警報配置", + "description": "哪些追蹤目標生成警報以及如何保留警報的設定。", + "enabled": { + "label": "開啟警報", + "description": "開啟或關閉此攝影機的警報生成。" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的目標標籤清單(例如:car、person)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態配置中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有引起警報的活動後等待多少秒後截止警報。" + } + }, + "detections": { + "label": "偵測配置", + "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。", + "enabled": { + "label": "開啟偵測", + "description": "開啟或關閉此攝影機的偵測事件。" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態配置中是否啟用了偵測。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 為審閱項生成描述和摘要。", + "enabled": { + "label": "開啟生成式 AI 描述", + "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "為警報開啟生成式 AI", + "description": "使用生成式 AI 為警報項生成描述。" + }, + "detections": { + "label": "為偵測開啟生成式 AI", + "description": "使用生成式 AI 為偵測項生成描述。" + }, + "image_source": { + "label": "審閱影像來源", + "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。" + }, + "additional_concerns": { + "label": "額外關注事項", + "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。" + }, + "preferred_language": { + "label": "首選語言", + "description": "向生成式 AI 提供商請求生成回應的首選語言。" + }, + "activity_context_prompt": { + "label": "活動上下文提示", + "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。" + } + }, + "description": "控制此攝影機的警報、偵測和生成式 AI 審閱總結的設定,這些設定會被介面與儲存功能使用。" + }, + "snapshots": { + "label": "快照", + "enabled": { + "label": "開啟快照", + "description": "開啟或關閉此攝影機的快照儲存。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在 API 生成的快照上疊加時間戳。" + }, + "bounding_box": { + "label": "邊界框疊加", + "description": "在 API 生成的快照上繪製追蹤目標的邊界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能儲存快照的區域。" + }, + "height": { + "label": "快照高度", + "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留設定,包括預設天數和按目標覆蓋。", + "default": { + "label": "預設保留", + "description": "保留快照的預設天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + }, + "objects": { + "label": "目標保留", + "description": "按目標覆蓋的快照保留天數。" + } + }, + "quality": { + "label": "快照品質", + "description": "儲存快照的編碼品質(0-100)。" + }, + "description": "此攝影機的追蹤目標 API 快照設定。" + }, + "timestamp_style": { + "label": "時間戳樣式", + "position": { + "label": "時間戳位置", + "description": "時間戳在影像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "時間戳格式", + "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。" + }, + "color": { + "label": "時間戳顏色", + "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。", + "red": { + "label": "紅色", + "description": "時間戳顏色的紅色分量(0-255)。" + }, + "green": { + "label": "綠色", + "description": "時間戳顏色的綠色分量(0-255)。" + }, + "blue": { + "label": "藍色", + "description": "時間戳顏色的藍色分量(0-255)。" + } + }, + "thickness": { + "label": "時間戳粗細", + "description": "時間戳文字的線條粗細。" + }, + "effect": { + "label": "時間戳效果", + "description": "時間戳文字的視覺效果(none、solid、shadow)。" + }, + "description": "應用於錄影和快照的即時監控流中時間戳的樣式選項。" + }, + "audio_transcription": { + "label": "音訊轉錄", + "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。", + "live_enabled": { + "label": "即時監控轉寫", + "description": "在接收到音訊時開啟即時監控持續轉寫。" + }, + "enabled": { + "label": "開啟轉錄", + "description": "開啟或關閉手動觸發的音訊事件轉寫。" + }, + "enabled_in_config": { + "label": "原始轉寫狀態" + } + }, + "semantic_search": { + "label": "語意搜尋", + "triggers": { + "label": "觸發器", + "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。", + "friendly_name": { + "label": "友好名稱", + "description": "在 UI 中為此觸發器顯示的可選友好名稱。" + }, + "enabled": { + "label": "開啟此觸發器", + "description": "啟用或停用此語意搜尋觸發器。" + }, + "type": { + "label": "觸發器型別", + "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。" + }, + "data": { + "label": "觸發器內容", + "description": "要與追蹤目標匹配的文字短語或縮圖 ID。" + }, + "threshold": { + "label": "觸發器閾值", + "description": "啟用此觸發器所需的最小相似度分數(0-1)。" + }, + "actions": { + "label": "觸發器操作", + "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。" + } + }, + "description": "語意搜尋設定,用於構建和查詢目標嵌入以查詢相似項目。" + }, + "face_recognition": { + "label": "人臉辨識", + "enabled": { + "label": "開啟人臉辨識", + "description": "開啟或關閉人臉辨識。" + }, + "min_area": { + "label": "最小人臉區域", + "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。" + }, + "description": "該攝影機的人臉偵測與辨識設定。" + }, + "lpr": { + "label": "車牌辨識", + "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。", + "enabled": { + "label": "開啟車牌辨識", + "description": "在此攝影機上啟用或停用車牌辨識。" + }, + "min_area": { + "label": "最小車牌區域", + "description": "嘗試辨識所需的最小車牌區域(像素)。" + }, + "enhancement": { + "label": "增強級別", + "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。" + }, + "expire_time": { + "label": "過期秒數", + "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。" + } + }, + "profiles": { + "label": "設定檔", + "description": "可在執行時切換指定命名的設定檔,支援區域性覆蓋引數。" + }, + "onvif": { + "label": "ONVIF", + "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。", + "host": { + "label": "ONVIF 主機", + "description": "此攝影機 ONVIF 服務的主機(和可選協議)。" + }, + "port": { + "label": "ONVIF 埠", + "description": "ONVIF 服務的埠號。" + }, + "user": { + "label": "ONVIF 使用者名稱", + "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密碼", + "description": "ONVIF 身份驗證的密碼。" + }, + "tls_insecure": { + "label": "停用 TLS 驗證", + "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。" + }, + "profile": { + "label": "ONVIF 設定檔", + "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。" + }, + "autotracking": { + "label": "自動追蹤", + "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。", + "enabled": { + "label": "開啟自動追蹤", + "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。" + }, + "calibrate_on_startup": { + "label": "啟動時校準", + "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。" + }, + "zooming": { + "label": "變焦模式", + "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。" + }, + "zoom_factor": { + "label": "變焦因子", + "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。" + }, + "track": { + "label": "追蹤目標", + "description": "應觸發自動追蹤的目標型別清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域之一才能開始自動追蹤。" + }, + "return_preset": { + "label": "返回預設", + "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。" + }, + "timeout": { + "label": "返回超時", + "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。" + }, + "movement_weights": { + "label": "移動權重", + "description": "由攝影機校準自動生成的校準值。請勿手動修改。" + }, + "enabled_in_config": { + "label": "原始自動追蹤狀態", + "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。" + } + }, + "ignore_time_mismatch": { + "label": "忽略時間不匹配", + "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。" + } + }, + "best_image_timeout": { + "label": "最佳影像超時", + "description": "等待具有最高置信度分數的影像的時間。" + }, + "type": { + "label": "攝影機型別", + "description": "攝影機型別" + }, + "ui": { + "label": "攝影機頁面", + "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。", + "order": { + "label": "UI 順序", + "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。" + }, + "dashboard": { + "label": "在 UI 中顯示", + "description": "切換此攝影機在 Frigate 頁面的所有位置是否可見。停用此項將需要手動編輯配置才能在頁面中再次檢視此攝影機。" + } + }, + "webui_url": { + "label": "攝影機 URL", + "description": "從系統頁面直接存取攝影機管理後臺的 URL" + }, + "zones": { + "label": "區域", + "description": "區域允許您定義幀的特定區域,以便確定目標是否在特定區域內。", + "friendly_name": { + "label": "區域名稱", + "description": "區域的友好名稱,顯示在 Frigate UI 中。如果未設定,將使用區域名稱的格式化版本。" + }, + "enabled": { + "label": "開啟", + "description": "開啟或關閉此區域。停用的區域在執行時將被忽略。" + }, + "enabled_in_config": { + "label": "保持區域原始狀態的跟蹤。" + }, + "filters": { + "label": "區域過濾器", + "description": "應用於此區域內目標的過濾器。用於減少誤報或限制哪些目標被認為存在於區域內。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "coordinates": { + "label": "座標", + "description": "定義區域區域的多邊形座標。可以是逗號分隔的字串或座標字串清單。座標應該是相對的(0-1)或絕對的(傳統)。" + }, + "distances": { + "label": "真實世界距離", + "description": "區域四邊形每邊的可選真實世界距離,用於速度或距離計算。如果設定,必須恰好有 4 個值。" + }, + "inertia": { + "label": "慣性幀數", + "description": "目標必須在區域內被連續偵測多少幀才能被認為存在。有助於過濾掉短暫偵測。" + }, + "loitering_time": { + "label": "徘徊秒數", + "description": "目標必須在區域內停留多少秒才能被視為徘徊。設定為 0 可停用徘徊偵測。" + }, + "speed_threshold": { + "label": "最小速度", + "description": "目標被認為存在於區域所需的最小速度(如果設定了距離,則為真實世界單位)。用於基於速度的區域觸發器。" + }, + "objects": { + "label": "觸發目標", + "description": "可以觸發此區域的目標型別清單(來自標籤對映)。可以是字串或字串清單。如果為空,則考慮所有目標。" + } + }, + "enabled_in_config": { + "label": "原始攝影機狀態", + "description": "保持攝影機的原始狀態跟蹤。" } } diff --git a/web/public/locales/zh-Hant/config/global.json b/web/public/locales/zh-Hant/config/global.json index 0f254ab830..1b973f1c2e 100644 --- a/web/public/locales/zh-Hant/config/global.json +++ b/web/public/locales/zh-Hant/config/global.json @@ -2,7 +2,8 @@ "audio": { "label": "音訊事件", "enabled": { - "label": "啟用音訊偵測" + "label": "啟用音訊偵測", + "description": "為所有攝影機啟用或停用音訊事件偵測;可按攝影機覆蓋。" }, "max_not_heard": { "label": "結束逾時", @@ -15,6 +16,1585 @@ "listen": { "label": "監聽的音訊類型", "description": "要偵測的音訊事件類型清單(例如:狗吠、火警、尖叫、說話、大叫)。" + }, + "description": "所有攝影機的基於音訊的事件偵測設定;可按攝影機覆蓋。", + "filters": { + "label": "音訊過濾器", + "description": "按音訊型別的過濾器設定,如用於減少誤報的置信度閾值。", + "threshold": { + "label": "最低音訊置信度", + "description": "音訊事件被計入的最低置信度閾值。" + } + }, + "enabled_in_config": { + "label": "原始音訊狀態", + "description": "指示原始靜態設定檔中是否開啟了音訊偵測。" + }, + "num_threads": { + "label": "偵測執行緒", + "description": "用於音訊偵測處理的執行緒數量。" + } + }, + "version": { + "label": "當前配置版本", + "description": "用於標識當前生效配置的版本號(數字或字串均可),幫助辨識配置遷移或格式是否發生變更。" + }, + "safe_mode": { + "label": "安全模式", + "description": "開啟後,Frigate 將以安全模式啟動,將會關閉部分功能,以便排查問題。" + }, + "environment_vars": { + "label": "環境變數", + "description": "用於在 Home Assistant OS 中為 Frigate 程序設定的環境變數。非 HAOS 使用者不能使用該配置項,而必須使用 Docker 的環境變數配置。" + }, + "logger": { + "label": "日誌", + "description": "控制預設日誌詳細程度,以及各元件的日誌級別覆蓋。", + "default": { + "label": "日誌等級", + "description": "預設全域性日誌詳細程度(除錯、資訊、警告、錯誤)。" + }, + "logs": { + "label": "單程序日誌級別", + "description": "按元件覆蓋日誌級別配置,用於提高或降低特定模組的日誌詳細程度。" + } + }, + "auth": { + "label": "身份驗證", + "description": "身份驗證和工作階段相關設定,包括 Cookie 和速率限制選項。", + "enabled": { + "label": "開啟身份驗證", + "description": "為 Frigate 頁面開啟原生身份驗證。" + }, + "reset_admin_password": { + "label": "重設管理員密碼", + "description": "開啟後,啟動時將重設管理員使用者密碼,並在日誌中列印新密碼。" + }, + "cookie_name": { + "label": "JWT Cookie 名稱", + "description": "用於儲存原生身份驗證 JWT 令牌的 Cookie 名稱。" + }, + "cookie_secure": { + "label": "安全 Cookie 標誌", + "description": "在身份驗證 Cookie 上設定安全標誌;使用 TLS 時應啟用此選項。" + }, + "session_length": { + "label": "工作階段時長", + "description": "基於 JWT 的工作階段持續時間(秒)。" + }, + "refresh_time": { + "label": "工作階段重新整理視窗", + "description": "當工作階段距離過期時間在此秒數範圍內時,將工作階段重新整理回完整時長。" + }, + "failed_login_rate_limit": { + "label": "登入失敗限制", + "description": "用於限制登入失敗嘗試次數的規則,以減少暴力破解攻擊。" + }, + "trusted_proxies": { + "label": "受信任的代理", + "description": "用於確定客戶端 IP 以進行速率限制的受信任代理 IP 清單。" + }, + "hash_iterations": { + "label": "雜湊迭代次數", + "description": "對使用者密碼進行雜湊處理時使用的 PBKDF2-SHA256 迭代次數。" + }, + "roles": { + "label": "權限組對映", + "description": "將權限組對映到攝影機清單。空清單表示該權限組可以存取所有攝影機。" + }, + "admin_first_time_login": { + "label": "管理員首次登入標誌", + "description": "啟用後,UI 可能會在登入頁面顯示幫助連結,告知使用者如何在管理員密碼重設後登入。 " + } + }, + "database": { + "label": "資料庫", + "description": "Frigate 用於儲存追蹤目標和錄影元資料的 SQLite 資料庫設定。", + "path": { + "label": "資料庫路徑", + "description": "Frigate SQLite 資料庫檔案的儲存路徑。" + } + }, + "go2rtc": { + "label": "go2rtc", + "description": "整合的 go2rtc 轉發服務設定,用於即時監控流轉發和轉碼。" + }, + "mqtt": { + "label": "MQTT", + "description": "連線到 MQTT 代理併發布遙測資料、快照和事件詳情的設定。", + "enabled": { + "label": "開啟 MQTT", + "description": "啟用或停用 MQTT 整合,用於狀態、事件和快照。" + }, + "host": { + "label": "MQTT 主機", + "description": "MQTT 代理的主機名或 IP 地址。" + }, + "port": { + "label": "MQTT 埠", + "description": "MQTT 代理的埠(普通 MQTT 通常為 1883)。" + }, + "topic_prefix": { + "label": "主題字首", + "description": "所有 Frigate 主題的 MQTT 主題字首;如果執行多個例項,必須唯一。" + }, + "client_id": { + "label": "客戶端 ID", + "description": "連線到 MQTT 代理時使用的客戶端辨識符號;每個例項應該唯一。" + }, + "stats_interval": { + "label": "統計資訊間隔", + "description": "向 MQTT 釋出系統和攝影機統計資訊的時間間隔(秒)。" + }, + "user": { + "label": "MQTT 使用者名稱", + "description": "可選的 MQTT 使用者名稱;可以透過環境變數或金鑰提供。" + }, + "password": { + "label": "MQTT 密碼", + "description": "可選的 MQTT 密碼;可以透過環境變數或金鑰提供。" + }, + "tls_ca_certs": { + "label": "TLS CA 證書", + "description": "用於 TLS 連線到代理的 CA 證書路徑(用於自簽名證書)。" + }, + "tls_client_cert": { + "label": "客戶端證書", + "description": "TLS 雙向認證的客戶端證書路徑;使用客戶端證書時不要設定使用者名稱/密碼。" + }, + "tls_client_key": { + "label": "客戶端金鑰", + "description": "客戶端證書的私鑰路徑。" + }, + "tls_insecure": { + "label": "TLS 不安全連線", + "description": "透過跳過主機名驗證允許不安全的 TLS 連線(不推薦)。" + }, + "qos": { + "label": "MQTT QoS", + "description": "MQTT 釋出/訂閱的服務品質級別(0、1 或 2)。" + } + }, + "notifications": { + "label": "通知", + "description": "為所有攝影機啟用和控制通知的設定;可按攝影機覆蓋。", + "enabled": { + "label": "開啟通知", + "description": "為所有攝影機啟用或停用通知;可按攝影機覆蓋。" + }, + "email": { + "label": "通知郵箱", + "description": "用於推送通知或某些通知提供商要求的郵箱地址。" + }, + "cooldown": { + "label": "冷卻時間", + "description": "通知之間的冷卻時間(秒),以避免向收件人傳送垃圾資訊。" + }, + "enabled_in_config": { + "label": "原始通知狀態", + "description": "指示原始靜態配置中是否啟用了通知。" + } + }, + "networking": { + "label": "網路", + "description": "網路相關設定,如 Frigate 端點的 IPv6 啟用。", + "ipv6": { + "label": "IPv6 配置", + "description": "Frigate 網路服務的 IPv6 特定設定。", + "enabled": { + "label": "開啟 IPv6", + "description": "在適用的情況下為 Frigate 服務(API 和 UI)啟用 IPv6 支援。" + } + }, + "listen": { + "label": "監聽埠配置", + "description": "內部和外部監聽埠的配置。此選項適用於高階使用者。對於大多數用例,建議在 Docker compose 檔案的 ports 部分進行更改。", + "internal": { + "label": "內部埠", + "description": "Frigate 的內部監聽埠(預設 5000)。" + }, + "external": { + "label": "外部埠", + "description": "Frigate 的外部監聽埠(預設 8971)。" + } + } + }, + "proxy": { + "label": "代理", + "description": "用於將 Frigate 整合到傳遞已認證使用者頭的反向代理後面的設定。", + "header_map": { + "label": "請求頭對映", + "description": "將傳入的代理請求頭對映到 Frigate 使用者和權限組欄位,用於基於代理的身份驗證。", + "user": { + "label": "使用者請求頭", + "description": "包含上游代理提供的已認證使用者名稱的請求頭。" + }, + "role": { + "label": "權限組請求頭", + "description": "包含來自上游代理的已認證使用者權限組或使用者組的請求頭。" + }, + "role_map": { + "label": "權限組對映", + "description": "將上游組值對映到 Frigate 權限組(例如將管理員組對映到管理員權限組)。" + } + }, + "logout_url": { + "label": "登出 URL", + "description": "透過代理登出時重定向使用者的 URL。" + }, + "auth_secret": { + "label": "代理金鑰", + "description": "與 X-Proxy-Secret 請求頭進行比對的可選金鑰,用於驗證受信任的代理。" + }, + "default_role": { + "label": "預設權限組", + "description": "當沒有權限組對映適用時分配給代理認證使用者的預設權限組(admin 或 viewer)。" + }, + "separator": { + "label": "分隔符", + "description": "用於分割代理請求頭中多個值的字元。" + } + }, + "telemetry": { + "label": "遙測", + "description": "系統遙測和統計選項,包括 GPU 和網路頻寬監控。", + "network_interfaces": { + "label": "網路介面", + "description": "要監控頻寬統計資訊的網路介面名稱字首清單。" + }, + "stats": { + "label": "系統統計", + "description": "用於啟用/停用各種系統和 GPU 統計資訊收集的選項。", + "amd_gpu_stats": { + "label": "AMD GPU 統計", + "description": "如果存在 AMD GPU,則啟用 AMD GPU 統計資訊收集。" + }, + "intel_gpu_stats": { + "label": "Intel GPU 統計", + "description": "如果存在 Intel GPU,則啟用 Intel GPU 統計資訊收集。" + }, + "network_bandwidth": { + "label": "網路頻寬", + "description": "為攝影機 ffmpeg 程序和偵測器啟用按程序網路頻寬監控(需要權限)。" + }, + "intel_gpu_device": { + "label": "Intel GPU 裝置", + "description": "當系統存在多個 Intel 顯示卡時,用於將顯示卡執行資料繫結到指定裝置的 PCI 匯流排地址或 DRM 裝置路徑(示例:/dev/dri/card1)。" + } + }, + "version_check": { + "label": "版本檢查", + "description": "啟用出站檢查以偵測是否有更新版本的 Frigate 可用。" + } + }, + "tls": { + "label": "TLS", + "description": "Frigate Web 端點(埠 8971)的 TLS 設定。", + "enabled": { + "label": "開啟 TLS", + "description": "為 Frigate 的網頁頁面和 API 的埠開啟 TLS 加密。" + } + }, + "ui": { + "label": "使用者介面", + "description": "使用者介面偏好設定,如時區、時間/日期格式和單位。", + "timezone": { + "label": "時區", + "description": "UI 中顯示的可選時區(如果未設定,則預設為瀏覽器本地時間)。" + }, + "time_format": { + "label": "時間格式", + "description": "UI 中使用的時間格式(browser、12hour 或 24hour)。" + }, + "date_style": { + "label": "日期樣式", + "description": "UI 中使用的日期樣式(full、long、medium、short)。" + }, + "time_style": { + "label": "時間樣式", + "description": "UI 中使用的時間樣式(full、long、medium、short)。" + }, + "unit_system": { + "label": "單位系統", + "description": "UI 和 MQTT 中使用的顯示單位系統(公制或英制)。" + } + }, + "detectors": { + "label": "偵測器硬體", + "description": "目標偵測器(CPU、GPU、ONNX 後端)的配置以及任何偵測器特定的模型設定。", + "type": { + "label": "型別" + }, + "model": { + "label": "偵測器特定的模型配置", + "description": "偵測器特定的模型配置選項(路徑、輸入尺寸等)。", + "path": { + "label": "自訂目標偵測模型路徑", + "description": "自訂偵測模型檔案的路徑(或使用 plus:// 指定 Frigate+ 模型)。" + }, + "labelmap_path": { + "label": "自訂目標偵測器的標籤對映(labelmap)", + "description": "偵測器標籤對映檔案(labelmap)路徑,用於將數字類別對映為文字標籤。" + }, + "width": { + "label": "目標偵測模型輸入寬度", + "description": "模型輸入張量(input tensor)的寬度(以像素為單位)。" + }, + "height": { + "label": "目標偵測模型輸入高度", + "description": "模型輸入張量(input tensor)的高度(以像素為單位)。" + }, + "labelmap": { + "label": "標籤對映(labelmap)自訂", + "description": "合併到標準標籤對映表中的覆蓋 / 重對映規則。" + }, + "attributes_map": { + "label": "目標標籤到其屬性標籤的對映", + "description": "用於繫結元資料的目標標籤 → 屬性標籤對映關係(例如:'car'→ ['license_plate'] 為將車牌屬性繫結到車輛上)。" + }, + "input_tensor": { + "label": "模型輸入張量形狀", + "description": "模型期望的張量格式(Tensor format):'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型輸入像素顏色格式", + "description": "模型期望的像素顏色空間:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型輸入資料型別", + "description": "模型輸入張量的資料型別(例如 'float32')。" + }, + "model_type": { + "label": "目標偵測模型型別", + "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。" + } + }, + "model_path": { + "label": "偵測器專用模型路徑", + "description": "所選偵測器需要時,需填寫其模型檔案的路徑。" + }, + "axengine": { + "label": "愛芯元智 NPU", + "description": "AXERA AX650N/AX8850N NPU 偵測器,透過 AXEngine 執行庫載入並執行編譯後的 .axmodel 模型檔案。" + }, + "cpu": { + "label": "CPU", + "description": "在主機 CPU 上執行 TensorFlow Lite 模型的 CPU TFLite 偵測器,無硬體加速。不推薦使用。", + "num_threads": { + "label": "偵測執行緒數", + "description": "用於基於 CPU 的推理的執行緒數。" + } + }, + "deepstack": { + "label": "DeepStack", + "description": "將影像傳送到遠端 DeepStack HTTP API 進行推理的 DeepStack/CodeProject.AI 偵測器。不推薦使用。", + "api_url": { + "label": "DeepStack API URL", + "description": "DeepStack API 的 URL。" + }, + "api_timeout": { + "label": "DeepStack API 超時時間(秒)", + "description": "DeepStack API 請求允許的最長時間。" + }, + "api_key": { + "label": "DeepStack API 金鑰(如需要)", + "description": "用於認證 DeepStack 服務的可選 API 金鑰。" + } + }, + "degirum": { + "label": "DeGirum", + "description": "透過 DeGirum 雲或本地推理服務執行模型的 DeGirum 偵測器。", + "location": { + "label": "推理位置", + "description": "DeGirum 推理引擎的位置(例如 '@cloud'、'127.0.0.1')。" + }, + "zoo": { + "label": "模型庫", + "description": "DeGirum 模型庫的路徑或 URL。" + }, + "token": { + "label": "DeGirum 雲令牌", + "description": "用於 DeGirum 雲存取的令牌。" + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "使用 EdgeTPU 委託執行為 Coral EdgeTPU 編譯的 TensorFlow Lite 模型的 EdgeTPU 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 EdgeTPU 推理的裝置(例如 'usb'、'pci')。" + } + }, + "hailo8l": { + "label": "Hailo-8/Hailo-8L", + "description": "使用 HEF 模型和 HailoRT SDK 在 Hailo 硬體上進行推理的 Hailo-8/Hailo-8L 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 Hailo 推理的裝置(例如 'PCIe'、'M.2')。" + } + }, + "memryx": { + "label": "MemryX", + "description": "在 MemryX 加速器上執行編譯的 DFP 模型的 MemryX MX3 偵測器。", + "device": { + "label": "裝置路徑", + "description": "用於 MemryX 推理的裝置(例如 'PCIe')。" + } + }, + "onnx": { + "label": "ONNX", + "description": "執行 ONNX 模型的 ONNX 偵測器;當可用時將使用可用的加速後端(CUDA/ROCm/OpenVINO)。", + "device": { + "label": "裝置型別", + "description": "用於 ONNX 推理的裝置(例如 'AUTO'、'CPU'、'GPU')。" + } + }, + "openvino": { + "label": "OpenVINO", + "description": "適用於 AMD 和 Intel CPU、Intel GPU 和 Intel VPU 硬體的 OpenVINO 偵測器。", + "device": { + "label": "裝置型別", + "description": "用於 OpenVINO 推理的裝置(例如 'CPU'、'GPU'、'NPU')。" + } + }, + "rknn": { + "label": "RKNN", + "description": "用於 Rockchip NPU 的 RKNN 偵測器;在 Rockchip 硬體上執行編譯的 RKNN 模型。", + "num_cores": { + "label": "使用的 NPU 核心數。", + "description": "要使用的 NPU 核心數(0 表示自動)。" + } + }, + "synaptics": { + "label": "Synaptics", + "description": "使用 Synap SDK 在 Synaptics 硬體上執行 .synap 格式模型的 Synaptics NPU 偵測器。" + }, + "teflon_tfl": { + "label": "Teflon", + "description": "使用 Mesa Teflon 委託庫在支援的 GPU 上加速推理的 TFLite Teflon 委託偵測器。" + }, + "tensorrt": { + "label": "TensorRT", + "description": "使用序列化的 TensorRT 引擎進行加速推理的 Nvidia Jetson 裝置 TensorRT 偵測器。", + "device": { + "label": "GPU 裝置索引", + "description": "要使用的 GPU 裝置索引。" + } + }, + "zmq": { + "label": "ZMQ IPC", + "description": "透過 ZeroMQ IPC 端點將推理解除安裝到外部程序的 ZMQ IPC 偵測器。", + "endpoint": { + "label": "ZMQ IPC 端點", + "description": "要連線的 ZMQ 端點。" + }, + "request_timeout_ms": { + "label": "ZMQ 請求超時(毫秒)", + "description": "ZMQ 請求的超時時間(毫秒)。" + }, + "linger_ms": { + "label": "ZMQ 套接字逗留時間(毫秒)", + "description": "套接字逗留時間(毫秒)。" + } + } + }, + "model": { + "label": "偵測模型", + "description": "用於配置自訂目標偵測模型及其輸入形狀的設定。", + "path": { + "label": "自訂目標偵測模型路徑", + "description": "自訂偵測模型檔案的路徑(或 Frigate+ 模型的 plus://)。" + }, + "labelmap_path": { + "label": "自訂目標偵測器的標籤對映", + "description": "將數字類別對映到偵測器字串標籤的標籤對映檔案路徑。" + }, + "width": { + "label": "目標偵測模型輸入寬度", + "description": "模型輸入張量的寬度(像素)。" + }, + "height": { + "label": "目標偵測模型輸入高度", + "description": "模型輸入張量的高度(像素)。" + }, + "labelmap": { + "label": "標籤對映自訂", + "description": "要合併到標準標籤對映中的覆蓋或重對映條目。" + }, + "attributes_map": { + "label": "目標標籤到屬性標籤的對映", + "description": "從目標標籤到屬性標籤的對映,用於附加元資料(例如 'car' -> ['license_plate'])。" + }, + "input_tensor": { + "label": "模型輸入張量形狀", + "description": "模型期望的張量格式:'nhwc' 或 'nchw'。" + }, + "input_pixel_format": { + "label": "模型輸入像素顏色格式", + "description": "模型期望的像素色彩空間:'rgb'、'bgr' 或 'yuv'。" + }, + "input_dtype": { + "label": "模型輸入資料型別", + "description": "模型輸入張量的資料型別(例如 'float32')。" + }, + "model_type": { + "label": "目標偵測模型型別", + "description": "某些偵測器用於最佳化的偵測器模型架構型別(ssd、yolox、yolonas)。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "用於生成目標描述和審閱摘要的整合生成式 AI 提供商設定。", + "api_key": { + "label": "API 金鑰", + "description": "某些提供商要求的 API 金鑰(也可以透過環境變數設定)。" + }, + "base_url": { + "label": "基礎 URL", + "description": "自託管或相容提供商的基礎 URL(例如 Ollama 例項)。" + }, + "model": { + "label": "模型", + "description": "用於生成描述或摘要的提供商模型。" + }, + "provider": { + "label": "提供商", + "description": "要使用的生成式 AI 提供商(例如:ollama、gemini、openai 等。國產大模型廠商可使用 openai 介面)。" + }, + "roles": { + "label": "功能", + "description": "生成式 AI 功能(對話、描述、嵌入);每個功能單獨一個提供商。" + }, + "provider_options": { + "label": "提供商選項", + "description": "要傳遞給生成式 AI 客戶端的、與服務提供商相關的額外配置項。" + }, + "runtime_options": { + "label": "執行時選項", + "description": "每次推理呼叫時傳遞給提供商的執行時選項。" + } + }, + "birdseye": { + "label": "鳥瞰圖", + "description": "將多路攝影機畫面合併為統一佈局的鳥瞰合成檢視設定。", + "enabled": { + "label": "開啟鳥瞰圖", + "description": "開啟或關閉鳥瞰圖功能。" + }, + "mode": { + "label": "追蹤模式", + "description": "在鳥瞰檢視中包含攝影機的模式:'objects'(目標)、'motion'(動作)或 'continuous'(持續)。" + }, + "restream": { + "label": "轉發 RTSP", + "description": "將鳥瞰圖輸出作為 RTSP 流重新轉發;啟用此功能將使鳥瞰圖持續執行。" + }, + "width": { + "label": "寬度", + "description": "合成的鳥瞰幀的輸出寬度(像素)。" + }, + "height": { + "label": "高度", + "description": "合成的鳥瞰幀的輸出高度(像素)。" + }, + "quality": { + "label": "編碼品質", + "description": "鳥瞰圖 mpeg1 流的編碼品質(1 最高品質,31 最低)。" + }, + "inactivity_threshold": { + "label": "非活動閾值", + "description": "攝影機停止在鳥瞰圖中顯示的非活動秒數。" + }, + "layout": { + "label": "佈局", + "description": "鳥瞰圖合成的佈局選項。", + "scaling_factor": { + "label": "縮放因子", + "description": "佈局計算器使用的縮放因子(範圍 1.0 到 5.0)。" + }, + "max_cameras": { + "label": "最大攝影機數", + "description": "鳥瞰圖中同時顯示的最大攝影機數量;顯示最近的攝影機。" + } + }, + "idle_heartbeat_fps": { + "label": "空閒心跳 FPS", + "description": "空閒時重新發送最後一個合成鳥瞰幀的每秒幀數;設為 0 則停用。" + }, + "order": { + "label": "排序位置", + "description": "用於控制攝影機在鳥瞰檢視佈局中排序位置的數值。" + } + }, + "detect": { + "label": "目標偵測", + "description": "用於執行目標偵測、初始化追蹤器的偵測模組設定。", + "enabled": { + "label": "開啟目標偵測", + "description": "為所有攝影機啟用或停用目標偵測,可按攝影機覆蓋。" + }, + "height": { + "label": "偵測畫面高度", + "description": "用於配置偵測流的畫面高度(像素);留空則使用原始影片流解析度。" + }, + "width": { + "label": "偵測畫面寬度", + "description": "用於配置偵測流的畫面寬度(像素);留空則使用原始影片流解析度。" + }, + "fps": { + "label": "偵測幀率", + "description": "偵測時希望使用的幀率;數值越低,CPU 佔用越小(推薦值為 5,僅在追蹤極高速運動的目標時才設定更高數值,最高不建議超過 10)。" + }, + "min_initialized": { + "label": "最小初始化幀數", + "description": "建立追蹤目標前,需要連續偵測到目標的次數。數值越大,錯誤觸發的追蹤越少。預設值為幀率除以 2。" + }, + "max_disappeared": { + "label": "最大消失幀數", + "description": "追蹤目標在連續多少幀未被偵測到時,將被判定為已消失。" + }, + "stationary": { + "label": "靜止目標配置", + "description": "用於偵測和管理長時間靜止目標的相關設定。", + "interval": { + "label": "靜止間隔", + "description": "設定每隔多少幀執行一次偵測,用於確認目標是否處於靜止狀態。" + }, + "threshold": { + "label": "靜止閾值", + "description": "目標需要連續多少幀位置不變,才會被標記為靜止狀態。" + }, + "max_frames": { + "label": "最大幀數", + "description": "限制靜止目標最大追蹤時長(以幀數為單位),超過將會停止追蹤。", + "default": { + "label": "預設最大幀數", + "description": "停止追蹤前,用於追蹤靜止目標的預設最大幀數。" + }, + "objects": { + "label": "目標最大幀數", + "description": "可對不同型別目標分別設定靜止追蹤的最大幀數(覆蓋全域性設定)。" + } + }, + "classifier": { + "label": "開啟視覺分類器", + "description": "使用視覺分類器,即使偵測框有輕微抖動,也能準確判斷物體是否為靜止。" + } + }, + "annotation_offset": { + "label": "標記偏移量", + "description": "偵測標記的時間偏移量(毫秒),用於讓時間軸上的偵測框與錄影畫面更精準對齊;可設定為正數或負數。" + } + }, + "ffmpeg": { + "label": "FFmpeg", + "description": "FFmpeg 編解碼相關設定,包含可執行檔案路徑、命令列引數、硬體加速選項,以及按不同功能劃分的輸出引數。", + "path": { + "label": "FFmpeg 路徑", + "description": "要使用的 FFmpeg 可執行檔案路徑,或版本別名(如 \"5.0\" 或 \"7.0\")。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "傳遞給 FFmpeg 程序的全域性引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "用於 FFmpeg 的硬體加速引數。建議使用對應硬體廠商的預設配置。" + }, + "input_args": { + "label": "輸入引數", + "description": "應用於 FFmpeg 輸入影片流的輸入引數。" + }, + "output_args": { + "label": "輸出引數", + "description": "用於不同 FFmpeg 功能(如偵測、錄製)的預設輸出引數。", + "detect": { + "label": "偵測輸出引數", + "description": "偵測功能影片流的預設輸出引數。" + }, + "record": { + "label": "錄製輸出引數", + "description": "錄製功能影片流的預設輸出引數。" + } + }, + "retry_interval": { + "label": "FFmpeg 重試時間", + "description": "攝影機影片流異常斷開後,重新連線前的等待時間。預設為 10 秒。" + }, + "apple_compatibility": { + "label": "Apple 相容性", + "description": "錄製 H.265 影片時啟用 HEVC 標記,以提升對 Apple 裝置播放的相容性。" + }, + "gpu": { + "label": "GPU 索引", + "description": "在啟用硬體加速時,預設使用的 GPU 索引。" + }, + "inputs": { + "label": "攝影機輸入影片流", + "description": "該攝影機的所有輸入流配置清單(包含路徑和功能)。", + "path": { + "label": "輸入路徑", + "description": "攝影機輸入影片流的地址或路徑。" + }, + "roles": { + "label": "輸入流功能", + "description": "定義該影片流的功能。" + }, + "global_args": { + "label": "FFmpeg 全域性引數", + "description": "該輸入影片流使用的 FFmpeg 全域性通用引數。" + }, + "hwaccel_args": { + "label": "硬體加速引數", + "description": "該輸入影片流的硬體加速引數。" + }, + "input_args": { + "label": "輸入引數", + "description": "該影片流特定的輸入引數。" + } + } + }, + "live": { + "label": "即時監控觀看", + "description": "用於控制 JSMPEG 即時流解析度與畫質的設定。此設定不影響使用 go2rtc 進行即時預覽的攝影機。", + "streams": { + "label": "即時監控流名稱", + "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。" + }, + "height": { + "label": "即時監控高度", + "description": "在網頁頁面中渲染 jsmpeg 即時監控流的高度(像素);必須小於等於偵測流高度。" + }, + "quality": { + "label": "即時監控品質", + "description": "jsmpeg 流的編碼品質(1 最高,31 最低)。" + } + }, + "motion": { + "label": "畫面變動偵測", + "description": "應用於攝影機的預設動作偵測設定,除非按攝影機覆蓋。", + "enabled": { + "label": "開啟畫面變動偵測", + "description": "為所有攝影機啟用或停用動作偵測;可按攝影機覆蓋。" + }, + "threshold": { + "label": "畫面變動閾值", + "description": "畫面變動偵測器使用的像素差異閾值;數值越高靈敏度越低(範圍 1-255)。" + }, + "lightning_threshold": { + "label": "閃電閾值", + "description": "用於偵測和忽略短暫閃電閃爍的閾值(數值越低越敏感,範圍 0.3 到 1.0)。這不會完全阻止畫面變動偵測;只是當超過閾值時偵測器會停止分析額外的幀。在此類事件期間仍會建立基於畫面變動的錄影。" + }, + "skip_motion_threshold": { + "label": "跳過畫面變動閾值", + "description": "如果單幀中畫面變化超過此比例,偵測器將判定為無畫面變動並立即重新校準。這可以節省 CPU 並減少閃電、風暴等情況下的誤報,但也可能會錯過真正的事件,如 PTZ 攝影機自動追蹤目標。你需要權衡取捨:是否犧牲少量錄製片段,換取更少無效影片與更低的誤檢。保持為空即可關閉該功能。" + }, + "improve_contrast": { + "label": "改善對比度", + "description": "在畫面變動分析之前對幀應用對比度改善以幫助偵測。" + }, + "contour_area": { + "label": "輪廓區域", + "description": "畫面變動輪廓被計入所需的最小輪廓區域(像素)。" + }, + "delta_alpha": { + "label": "Delta alpha", + "description": "用於畫面變動計算的幀差異中使用的 alpha 混合因子。" + }, + "frame_alpha": { + "label": "畫面 alpha 通道", + "description": "畫面變動預處理時混合畫面所使用的 alpha 值。" + }, + "frame_height": { + "label": "畫面高度", + "description": "計算畫面變動時縮放畫面的高度(像素)。" + }, + "mask": { + "label": "遮罩座標", + "description": "定義用於包含/排除區域的畫面變動遮罩多邊形的有序 x,y 座標。" + }, + "mqtt_off_delay": { + "label": "MQTT 關閉延遲", + "description": "在釋出 MQTT 'off' 狀態之前,最後一次畫面變動後等待的秒數。" + }, + "enabled_in_config": { + "label": "原始畫面變動狀態", + "description": "指示原始靜態配置中是否啟用了畫面變動偵測。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "objects": { + "label": "目標", + "description": "目標追蹤預設設定,包括要追蹤的標籤和按目標的過濾器。", + "track": { + "label": "要追蹤的目標", + "description": "所有攝影機要追蹤的目標標籤清單;可按攝影機覆蓋。" + }, + "filters": { + "label": "目標過濾器", + "description": "應用於偵測到的目標以減少誤報的過濾器(區域、比例、置信度)。", + "min_area": { + "label": "最小目標區域", + "description": "此目標型別所需的最小邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "max_area": { + "label": "最大目標區域", + "description": "此目標型別允許的最大邊界框區域(像素或百分比)。可以是像素(整數)或百分比(0.000001 到 0.99 之間的浮點數)。" + }, + "min_ratio": { + "label": "最小縱橫比", + "description": "邊界框所需的最小寬高比。" + }, + "max_ratio": { + "label": "最大縱橫比", + "description": "邊界框允許的最大寬高比。" + }, + "threshold": { + "label": "置信度閾值", + "description": "目標被視為真正陽性所需的平均偵測置信度閾值。" + }, + "min_score": { + "label": "最小置信度", + "description": "目標被計入所需的最小單幀偵測置信度。" + }, + "mask": { + "label": "過濾器遮罩", + "description": "定義此過濾器在幀內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" + } + }, + "mask": { + "label": "目標遮罩", + "description": "用於防止在指定區域進行目標偵測的遮罩多邊形。" + }, + "raw_mask": { + "label": "原始遮罩" + }, + "genai": { + "label": "生成式 AI 目標配置", + "description": "用於傳送畫面給生成式 AI 進行生成和描述追蹤目標的選項。", + "enabled": { + "label": "開啟生成式 AI", + "description": "預設開啟生成式 AI 生成追蹤目標的描述。" + }, + "use_snapshot": { + "label": "使用快照", + "description": "使用目標快照而不是縮圖給生成式 AI 進行描述生成。" + }, + "prompt": { + "label": "字幕提示", + "description": "使用生成式 AI 生成描述時使用的預設提示模板。" + }, + "object_prompts": { + "label": "目標提示", + "description": "按目標設定提示詞,讓生成式 AI 對不同標籤的輸出進行定製。" + }, + "objects": { + "label": "生成式 AI 目標", + "description": "預設傳送給生成式 AI 的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域,才會觸發生成式 AI 描述生成。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 的縮圖用於除錯和審閱。" + }, + "send_triggers": { + "label": "生成式 AI 觸發器", + "description": "定義畫面幀應在何時傳送給生成式 AI(如偵測結束時、更新後等)。", + "tracked_object_end": { + "label": "結束時傳送", + "description": "目標追蹤結束時向生成式 AI 傳送請求。" + }, + "after_significant_updates": { + "label": "生成式 AI 提前觸發", + "description": "在追蹤目標發生指定次數的重要變化後,向生成式 AI 傳送請求。" + } + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "表示在原始靜態配置中是否已啟用生成式 AI。" + } + } + }, + "record": { + "label": "錄影", + "description": "應用於攝影機的錄影和保留設定,除非按攝影機覆蓋。", + "enabled": { + "label": "開啟錄影", + "description": "為所有攝影機啟用或停用錄影;可按攝影機覆蓋。" + }, + "expire_interval": { + "label": "錄影清理間隔", + "description": "清理過期錄影片段的間隔分鐘數。" + }, + "continuous": { + "label": "持續保留", + "description": "無論是否有追蹤目標或動作,保留錄影的天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "motion": { + "label": "動作保留", + "description": "無論是否有追蹤目標,由動作觸發的錄影保留天數。如果只想保留警報和偵測的錄影,請設定為 0。", + "days": { + "label": "保留天數", + "description": "保留錄影的天數。" + } + }, + "detections": { + "label": "偵測保留", + "description": "偵測事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "alerts": { + "label": "警報保留", + "description": "警報事件的錄影保留設定,包括前後捕獲時長。", + "pre_capture": { + "label": "前捕獲秒數", + "description": "偵測事件之前包含在錄影中的秒數。" + }, + "post_capture": { + "label": "後捕獲秒數", + "description": "偵測事件之後包含在錄影中的秒數。" + }, + "retain": { + "label": "事件保留", + "description": "偵測事件錄影的保留設定。", + "days": { + "label": "保留天數", + "description": "保留偵測事件錄影的天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + } + } + }, + "export": { + "label": "匯出配置", + "description": "匯出錄影時使用的設定,如延時攝影和硬體加速。", + "hwaccel_args": { + "label": "匯出硬體加速引數", + "description": "用於匯出/轉碼操作的硬體加速引數。" + }, + "max_concurrent": { + "label": "最大併發匯出數", + "description": "同時可處理的最大匯出任務數量。" + } + }, + "preview": { + "label": "預覽配置", + "description": "控制介面中顯示的錄影預覽品質的設定。", + "quality": { + "label": "預覽品質", + "description": "預覽品質級別(very_low、low、medium、high、very_high)。" + } + }, + "enabled_in_config": { + "label": "原始錄影狀態", + "description": "指示原始靜態配置中是否啟用了錄影。" + } + }, + "review": { + "label": "審閱", + "description": "控制 UI 和儲存使用的警報、偵測和 GenAI 審閱摘要的設定。", + "alerts": { + "label": "警報配置", + "description": "哪些追蹤目標生成警報以及如何保留警報的設定。", + "enabled": { + "label": "開啟警報", + "description": "為所有攝影機啟用或停用警報生成;可按攝影機覆蓋。" + }, + "labels": { + "label": "警報標籤", + "description": "符合警報條件的目標標籤清單(例如:car、person)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為警報的區域;留空則允許任何區域。" + }, + "enabled_in_config": { + "label": "原始警報狀態", + "description": "追蹤原始靜態配置中是否啟用了警報。" + }, + "cutoff_time": { + "label": "警報截止時間", + "description": "在沒有引起警報的活動後等待多少秒後截止警報。" + } + }, + "detections": { + "label": "偵測配置", + "description": "用於設定哪些追蹤目標會生成偵測記錄(非警報類),以及偵測記錄的保留方式。", + "enabled": { + "label": "開啟偵測", + "description": "為所有攝影機啟用或停用偵測事件;可按攝影機覆蓋。" + }, + "labels": { + "label": "偵測標籤", + "description": "符合偵測事件條件的目標標籤清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能被視為偵測的區域;留空則允許任何區域。" + }, + "cutoff_time": { + "label": "偵測截止時間", + "description": "在沒有引起偵測的活動後等待多少秒後截止偵測。" + }, + "enabled_in_config": { + "label": "原始偵測狀態", + "description": "追蹤原始靜態配置中是否啟用了偵測。" + } + }, + "genai": { + "label": "生成式 AI 配置", + "description": "控制使用生成式 AI 為審閱項生成描述和摘要。", + "enabled": { + "label": "開啟生成式 AI 描述", + "description": "為審閱項開啟或關閉使用生成式 AI 生成描述和摘要。" + }, + "alerts": { + "label": "為警報開啟生成式 AI", + "description": "使用生成式 AI 為警報項生成描述。" + }, + "detections": { + "label": "為偵測開啟生成式 AI", + "description": "使用生成式 AI 為偵測項生成描述。" + }, + "image_source": { + "label": "審閱影像來源", + "description": "傳送給生成式 AI 的畫面來源('preview' 或 'recordings');'recordings' 使用更高品質的畫面幀,但會消耗更多的 token。" + }, + "additional_concerns": { + "label": "額外關注事項", + "description": "生成式 AI 在分析此攝影機的監控行為時,需要額外注意的事項或說明清單。" + }, + "debug_save_thumbnails": { + "label": "儲存縮圖", + "description": "儲存傳送給生成式 AI 提供商的縮圖用於除錯和審閱。" + }, + "enabled_in_config": { + "label": "原配置生成式 AI 狀態", + "description": "記錄在靜態配置中最初是否已啟用生成式 AI 審閱功能。" + }, + "preferred_language": { + "label": "首選語言", + "description": "向生成式 AI 提供商請求生成回應的首選語言。" + }, + "activity_context_prompt": { + "label": "活動上下文提示", + "description": "自訂提示詞,用於說明可疑行為與非可疑行為的界定,為生成式 AI 生成摘要提供上下文依據。" + } + } + }, + "snapshots": { + "label": "快照", + "description": "所有攝影機的追蹤目標 API 快照設定;可攝影機單獨配置覆蓋全域性配置。", + "enabled": { + "label": "開啟快照", + "description": "為所有攝影機啟用或停用儲存快照;可按攝影機覆蓋。" + }, + "timestamp": { + "label": "時間戳疊加", + "description": "在 API 生成的快照上疊加時間戳。" + }, + "bounding_box": { + "label": "邊界框疊加", + "description": "在 API 生成的快照上繪製追蹤目標的邊界框。" + }, + "crop": { + "label": "裁剪快照", + "description": "在 API 生成的快照裁剪到偵測到的目標邊界框。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能儲存快照的區域。" + }, + "height": { + "label": "快照高度", + "description": "將 API 生成的快照調整到的目標高度(像素);留空則保持原始大小。" + }, + "retain": { + "label": "快照保留", + "description": "快照的保留設定,包括預設天數和按目標覆蓋。", + "default": { + "label": "預設保留", + "description": "保留快照的預設天數。" + }, + "mode": { + "label": "保留模式", + "description": "保留模式:all(儲存所有片段)、motion(儲存有動作的片段)或 active_objects(儲存有活動目標的片段)。" + }, + "objects": { + "label": "目標保留", + "description": "按目標覆蓋的快照保留天數。" + } + }, + "quality": { + "label": "快照品質", + "description": "儲存快照的編碼品質(0-100)。" + } + }, + "timestamp_style": { + "label": "時間戳樣式", + "description": "應用於除錯檢視和快照的幀內時間戳樣式選項。", + "position": { + "label": "時間戳位置", + "description": "時間戳在影像上的位置(tl/tr/bl/br)。" + }, + "format": { + "label": "時間戳格式", + "description": "用於時間戳的日期時間格式字串(Python 日期時間格式程式碼)。" + }, + "color": { + "label": "時間戳顏色", + "description": "時間戳文字的 RGB 顏色值(所有值 0-255)。", + "red": { + "label": "紅色", + "description": "時間戳顏色的紅色分量(0-255)。" + }, + "green": { + "label": "綠色", + "description": "時間戳顏色的綠色分量(0-255)。" + }, + "blue": { + "label": "藍色", + "description": "時間戳顏色的藍色分量(0-255)。" + } + }, + "thickness": { + "label": "時間戳粗細", + "description": "時間戳文字的線條粗細。" + }, + "effect": { + "label": "時間戳效果", + "description": "時間戳文字的視覺效果(none、solid、shadow)。" + } + }, + "audio_transcription": { + "label": "音訊轉錄", + "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。", + "enabled": { + "label": "開啟音訊轉錄", + "description": "為所有攝影機啟用或停用自動音訊轉錄;可按攝影機覆蓋。" + }, + "language": { + "label": "轉錄語言", + "description": "用於轉錄/翻譯的語言程式碼(例如 'en' 表示英語)。請參閱 https://whisper-api.com/docs/languages/ 瞭解支援的語言程式碼。" + }, + "device": { + "label": "轉錄裝置", + "description": "執行轉錄模型的裝置金鑰(CPU/GPU)。目前僅支援 NVIDIA CUDA GPU 進行轉錄。" + }, + "model_size": { + "label": "模型大小", + "description": "用於離線音訊事件轉錄的模型大小。" + }, + "live_enabled": { + "label": "即時監控轉寫", + "description": "在接收到音訊時開啟即時監控持續轉寫。" + } + }, + "classification": { + "label": "目標分類", + "description": "用於最佳化目標標籤或狀態分類的分類模型設定。", + "bird": { + "label": "鳥類分類配置", + "description": "鳥類分類模型特定的設定。", + "enabled": { + "label": "鳥類分類", + "description": "啟用或停用鳥類分類。" + }, + "threshold": { + "label": "最小分數", + "description": "接受鳥類分類所需的最小分類分數。" + } + }, + "custom": { + "label": "自訂分類模型", + "description": "用於目標或狀態偵測的自訂分類模型配置。", + "enabled": { + "label": "開啟模型", + "description": "啟用或停用自訂分類模型。" + }, + "name": { + "label": "模型名稱", + "description": "要使用的自訂分類模型的辨識符號。" + }, + "threshold": { + "label": "分數閾值", + "description": "用於更改分類狀態的分數閾值。" + }, + "save_attempts": { + "label": "儲存嘗試", + "description": "為最近分類 UI 儲存多少次分類嘗試。" + }, + "object_config": { + "objects": { + "label": "分類目標", + "description": "要執行目標分類的目標型別清單。" + }, + "classification_type": { + "label": "分類型別", + "description": "應用的分類型別:'sub_label'(新增 sub_label)或其他支援的型別。" + } + }, + "state_config": { + "cameras": { + "label": "分類攝影機", + "description": "用於執行狀態分類的按攝影機裁剪和設定。", + "crop": { + "label": "分類裁剪", + "description": "用於在此攝影機上執行分類的裁剪座標。" + } + }, + "motion": { + "label": "動作時執行", + "description": "啟用後,當在指定裁剪區域內偵測到動作時執行分類。" + }, + "interval": { + "label": "分類間隔", + "description": "狀態分類的定期分類執行間隔(秒)。" + } + } + } + }, + "semantic_search": { + "label": "語意搜尋", + "description": "用於構建和查詢目標嵌入以查詢相似項的語意搜尋設定。", + "enabled": { + "label": "開啟語意搜尋", + "description": "啟用或停用語意搜尋功能。" + }, + "reindex": { + "label": "啟動時重建索引", + "description": "觸發將歷史追蹤目標完全重新索引到嵌入資料庫。" + }, + "model": { + "label": "語意搜尋模型或生成式 AI 服務名稱", + "description": "用於語意搜尋的嵌入模型(例如 'jinav1'),或具有嵌入功能(embeddings)的生成式 AI 服務名稱。" + }, + "model_size": { + "label": "模型大小", + "description": "選擇模型大小;'small' 在 CPU 上執行,'large' 通常需要 GPU。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + }, + "triggers": { + "label": "觸發器", + "description": "攝影機特定語意搜尋觸發器的操作和匹配條件。", + "friendly_name": { + "label": "友好名稱", + "description": "在 UI 中為此觸發器顯示的可選友好名稱。" + }, + "enabled": { + "label": "開啟此觸發器", + "description": "啟用或停用此語意搜尋觸發器。" + }, + "type": { + "label": "觸發器型別", + "description": "觸發器型別:'thumbnail'(與影像匹配)或 'description'(與文字匹配)。" + }, + "data": { + "label": "觸發器內容", + "description": "要與追蹤目標匹配的文字短語或縮圖 ID。" + }, + "threshold": { + "label": "觸發器閾值", + "description": "啟用此觸發器所需的最小相似度分數(0-1)。" + }, + "actions": { + "label": "觸發器操作", + "description": "觸發器匹配時要執行的操作清單(通知、sub_label、屬性)。" + } + } + }, + "face_recognition": { + "label": "人臉辨識", + "description": "所有攝影機的人臉偵測和辨識設定;可按攝影機覆蓋。", + "enabled": { + "label": "開啟人臉辨識", + "description": "為所有攝影機啟用或停用人臉辨識;可按攝影機覆蓋。" + }, + "model_size": { + "label": "模型大小", + "description": "用於人臉嵌入的模型大小(small/large);較大的可能需要 GPU。" + }, + "unknown_score": { + "label": "未知分數閾值", + "description": "低於此距離閾值的人臉被視為潛在匹配(數值越高越嚴格)。" + }, + "detection_threshold": { + "label": "偵測閾值", + "description": "將人臉偵測視為有效所需的最小偵測置信度。" + }, + "recognition_threshold": { + "label": "辨識閾值", + "description": "將兩張人臉視為匹配的人臉嵌入距離閾值。" + }, + "min_area": { + "label": "最小人臉區域", + "description": "需要嘗試進行人臉辨識的人臉偵測框最小大小(像素)。" + }, + "min_faces": { + "label": "最小人臉數", + "description": "在將辨識的子標籤應用於人員之前所需的最小人臉辨識次數。" + }, + "save_attempts": { + "label": "儲存嘗試", + "description": "為最近辨識 UI 保留的人臉辨識嘗試次數。" + }, + "blur_confidence_filter": { + "label": "模糊置信度過濾器", + "description": "根據影像模糊程度調整置信度分數,以減少低品質人臉的誤報。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + } + }, + "lpr": { + "label": "車牌辨識", + "description": "車牌辨識設定,包括偵測閾值、格式化和已知車牌。", + "enabled": { + "label": "開啟車牌辨識", + "description": "為所有攝影機啟用或停用車牌辨識;可按攝影機覆蓋。" + }, + "model_size": { + "label": "模型大小", + "description": "用於文字偵測/辨識的模型大小,大多數使用者應使用 'small',只有'small'模型支援中文。" + }, + "detection_threshold": { + "label": "偵測閾值", + "description": "開始對疑似車牌執行 OCR 的偵測置信度閾值。" + }, + "min_area": { + "label": "最小車牌區域", + "description": "嘗試辨識所需的最小車牌區域(像素)。" + }, + "recognition_threshold": { + "label": "辨識閾值", + "description": "辨識的車牌文字作為子標籤附加所需的置信度閾值。" + }, + "min_plate_length": { + "label": "最小車牌長度", + "description": "辨識的車牌被視為有效所需的最小字元數。" + }, + "format": { + "label": "車牌格式正則", + "description": "用於驗證辨識的車牌字串是否符合預期格式的可選正則表示式。" + }, + "match_distance": { + "label": "匹配距離", + "description": "將偵測到的車牌與已知車牌比較時允許的字元不匹配數。" + }, + "known_plates": { + "label": "已知車牌", + "description": "要特別追蹤或報警的車牌或正則表示式清單。" + }, + "enhancement": { + "label": "增強級別", + "description": "在 OCR 之前應用於車牌裁剪的增強級別(0-10);較高的值可能不總是改善結果,5 以上的級別可能僅適用於夜間車牌,應謹慎使用。" + }, + "debug_save_plates": { + "label": "儲存除錯車牌", + "description": "儲存車牌裁剪影像用於除錯 LPR 效能。" + }, + "device": { + "label": "裝置", + "description": "這是一個覆蓋選項,用於指定特定裝置。請參閱 https://onnxruntime.ai/docs/execution-providers/ 瞭解更多資訊" + }, + "replace_rules": { + "label": "替換規則", + "description": "用於在匹配之前規範化偵測到的車牌字串的正則替換規則。", + "pattern": { + "label": "正則模式" + }, + "replacement": { + "label": "替換字串" + } + }, + "expire_time": { + "label": "過期秒數", + "description": "未見到的車牌從追蹤器中過期的時間(秒)(僅適用於專用 LPR 攝影機)。" + } + }, + "camera_groups": { + "label": "攝影機分組", + "description": "用於在頁面中組織攝影機的命名攝影機分組配置。", + "cameras": { + "label": "攝影機清單", + "description": "此分組中包含的攝影機名稱陣列。" + }, + "icon": { + "label": "分組圖示", + "description": "在頁面中代表攝影機分組的圖示。" + }, + "order": { + "label": "排序順序", + "description": "用於在頁面中對攝影機分組進行排序的數字順序;數值越大越靠後。" + } + }, + "profiles": { + "label": "設定檔", + "description": "帶有別名的命名設定檔定義。攝影機設定檔必須引用此處定義的名稱。", + "friendly_name": { + "label": "別名", + "description": "在介面中顯示的此設定檔名稱,可以使用中文。" + } + }, + "active_profile": { + "label": "啟用設定檔", + "description": "當前啟用的設定檔名稱。僅在執行時使用,不會寫入 YAML 設定檔中。" + }, + "camera_mqtt": { + "label": "MQTT", + "description": "MQTT 影像釋出設定。", + "enabled": { + "label": "傳送影像", + "description": "為此攝影機啟用將目標快照影像釋出到 MQTT 主題。" + }, + "timestamp": { + "label": "新增時間戳", + "description": "在釋出到 MQTT 的影像上疊加時間戳。" + }, + "bounding_box": { + "label": "新增邊界框", + "description": "在透過 MQTT 釋出的影像上繪製邊界框。" + }, + "crop": { + "label": "裁剪影像", + "description": "將釋出到 MQTT 的影像裁剪到偵測到的目標邊界框。" + }, + "height": { + "label": "影像高度", + "description": "透過 MQTT 釋出的影像調整到的目標高度(像素)。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入才能釋出 MQTT 影像的區域。" + }, + "quality": { + "label": "JPEG 品質", + "description": "釋出到 MQTT 的影像的 JPEG 品質(0-100)。" + } + }, + "camera_ui": { + "label": "攝影機頁面", + "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。", + "order": { + "label": "UI 順序", + "description": "用於在頁面中排序攝影機的順序(只會影響預設儀表板和清單);數值越大則在越後面。" + }, + "dashboard": { + "label": "在 UI 中顯示", + "description": "切換此攝影機在 Frigate 頁面中是否可見。停用後需要手動編輯配置才能再次在頁面中檢視此攝影機。" + } + }, + "onvif": { + "label": "ONVIF", + "description": "此攝影機的 ONVIF 連線和 PTZ 自動追蹤設定。", + "host": { + "label": "ONVIF 主機", + "description": "此攝影機 ONVIF 服務的主機(和可選協議)。" + }, + "port": { + "label": "ONVIF 埠", + "description": "ONVIF 服務的埠號。" + }, + "user": { + "label": "ONVIF 使用者名稱", + "description": "ONVIF 身份驗證的使用者名稱;某些裝置需要管理員使用者才能使用 ONVIF。" + }, + "password": { + "label": "ONVIF 密碼", + "description": "ONVIF 身份驗證的密碼。" + }, + "tls_insecure": { + "label": "停用 TLS 驗證", + "description": "跳過 TLS 驗證並停用 ONVIF 的摘要認證(不安全;僅用於安全網路)。" + }, + "profile": { + "label": "ONVIF 設定檔", + "description": "用於 PTZ 控制的指定 ONVIF 媒體配置,將透過 Token 或名稱匹配。如果未手動指定,將自動選擇第一個包含有效 PTZ 配置的媒體配置。" + }, + "autotracking": { + "label": "自動追蹤", + "description": "使用 PTZ 攝影機移動自動追蹤移動目標並使其保持在畫面中心。", + "enabled": { + "label": "開啟自動追蹤", + "description": "啟用或停用偵測目標的自動 PTZ 攝影機追蹤。" + }, + "calibrate_on_startup": { + "label": "啟動時校準", + "description": "在啟動時測量 PTZ 電機速度以提高追蹤精度。Frigate 將在校準後用 movement_weights 更新配置。" + }, + "zooming": { + "label": "變焦模式", + "description": "控制變焦行為:disabled(僅平移/傾斜)、absolute(最相容)或 relative(同時平移/傾斜/變焦)。" + }, + "zoom_factor": { + "label": "變焦因子", + "description": "控制追蹤目標的變焦級別。數值越低保持更多場景可見;數值越高放大更近但可能丟失追蹤。數值範圍 0.1 到 0.75。" + }, + "track": { + "label": "追蹤目標", + "description": "應觸發自動追蹤的目標型別清單。" + }, + "required_zones": { + "label": "必需區域", + "description": "目標必須進入這些區域之一才能開始自動追蹤。" + }, + "return_preset": { + "label": "返回預設", + "description": "追蹤結束後返回的攝影機韌體中配置的 ONVIF 預設名稱。" + }, + "timeout": { + "label": "返回超時", + "description": "失去追蹤後等待多少秒後將攝影機返回到預設位置。" + }, + "movement_weights": { + "label": "移動權重", + "description": "由攝影機校準自動生成的校準值。請勿手動修改。" + }, + "enabled_in_config": { + "label": "原始自動追蹤狀態", + "description": "用於追蹤配置中是否啟用自動追蹤的內部欄位。" + } + }, + "ignore_time_mismatch": { + "label": "忽略時間不匹配", + "description": "忽略 ONVIF 通訊中攝影機和 Frigate 伺服器之間的時間同步差異。" } } } diff --git a/web/public/locales/zh-Hant/config/groups.json b/web/public/locales/zh-Hant/config/groups.json index 0967ef424b..5180463b90 100644 --- a/web/public/locales/zh-Hant/config/groups.json +++ b/web/public/locales/zh-Hant/config/groups.json @@ -1 +1,73 @@ -{} +{ + "audio": { + "global": { + "detection": "全域性偵測", + "sensitivity": "全域性靈敏度" + }, + "cameras": { + "detection": "偵測", + "sensitivity": "靈敏度" + } + }, + "timestamp_style": { + "global": { + "appearance": "全域性外觀" + }, + "cameras": { + "appearance": "外觀" + } + }, + "motion": { + "global": { + "sensitivity": "全域性靈敏度", + "algorithm": "全域性演算法" + }, + "cameras": { + "sensitivity": "靈敏度", + "algorithm": "演算法" + } + }, + "snapshots": { + "global": { + "display": "全域性顯示" + }, + "cameras": { + "display": "顯示" + } + }, + "detect": { + "global": { + "resolution": "全域性解析度", + "tracking": "全域性追蹤" + }, + "cameras": { + "resolution": "解析度", + "tracking": "追蹤" + } + }, + "objects": { + "global": { + "tracking": "全域性追蹤", + "filtering": "全域性篩選" + }, + "cameras": { + "tracking": "追蹤", + "filtering": "篩選" + } + }, + "record": { + "global": { + "retention": "全域性保留", + "events": "全域性事件" + }, + "cameras": { + "retention": "保留", + "events": "事件" + } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "攝影機特定的 FFmpeg 引數" + } + } +} diff --git a/web/public/locales/zh-Hant/config/validation.json b/web/public/locales/zh-Hant/config/validation.json index 0967ef424b..2c0274b3b6 100644 --- a/web/public/locales/zh-Hant/config/validation.json +++ b/web/public/locales/zh-Hant/config/validation.json @@ -1 +1,32 @@ -{} +{ + "minimum": "必須至少為 {{limit}}", + "maximum": "最大值不能超過 {{limit}}", + "exclusiveMinimum": "必須大於 {{limit}}", + "exclusiveMaximum": "必須小於 {{limit}}", + "minLength": "長度至少為 {{limit}} 個字元", + "maxLength": "長度最多為 {{limit}} 個字元", + "minItems": "至少包含 {{limit}} 項", + "maxItems": "最多包含 {{limit}} 項", + "pattern": "格式無效", + "required": "此欄位為必填項", + "type": "值型別無效", + "enum": "必須是允許的值之一", + "const": "值與預期的常量不匹配", + "uniqueItems": "所有項必須唯一", + "format": "格式無效", + "additionalProperties": "不允許未知屬性", + "oneOf": "必須完全匹配一個允許的模式", + "anyOf": "必須至少匹配一個允許的模式", + "proxy": { + "header_map": { + "roleHeaderRequired": "設定角色對應時必須要有 role 標頭。" + } + }, + "ffmpeg": { + "inputs": { + "rolesUnique": "每個角色只能分配給一個輸入串流。", + "detectRequired": "必須至少有一個輸入串流分配為 'detect' 角色。", + "hwaccelDetectOnly": "只有分配了 detect 角色的輸入串流才能定義硬體加速引數。" + } + } +} diff --git a/web/public/locales/zh-Hant/objects.json b/web/public/locales/zh-Hant/objects.json index 092506cdd4..2dc4ad5e8f 100644 --- a/web/public/locales/zh-Hant/objects.json +++ b/web/public/locales/zh-Hant/objects.json @@ -116,5 +116,14 @@ "nzpost": "紐西蘭郵政(NZ Post)", "postnord": "北歐郵政(PostNord)", "gls": "GLS 快遞", - "dpd": "DPD 快遞" + "dpd": "DPD 快遞", + "canada_post": "加拿大郵政", + "royal_mail": "英國皇家郵政", + "school_bus": "校車", + "skunk": "臭鼬", + "kangaroo": "袋鼠", + "baby": "嬰兒", + "baby_stroller": "嬰兒推車", + "rickshaw": "人力車", + "rodent": "齧齒動物" } diff --git a/web/public/locales/zh-Hant/views/chat.json b/web/public/locales/zh-Hant/views/chat.json index 0967ef424b..fced43e35e 100644 --- a/web/public/locales/zh-Hant/views/chat.json +++ b/web/public/locales/zh-Hant/views/chat.json @@ -1 +1,64 @@ -{} +{ + "documentTitle": "聊天 - Frigate", + "title": "Frigate 聊天", + "subtitle": "你的攝影機管理與智慧分析 AI 助手", + "placeholder": "嘗試問我任何事…", + "error": "出現錯誤,請稍後重試。", + "processing": "進行中…", + "toolsUsed": "使用:{{tools}}", + "showTools": "顯示工具({{count}})", + "hideTools": "隱藏工具", + "call": "呼叫", + "result": "結果", + "arguments": "引數:", + "response": "回應:", + "attachment_chip_label": "在 {{camera}} 的 {{label}}", + "attachment_chip_remove": "移除附件", + "open_in_explore": "從瀏覽中開啟", + "attach_event_aria": "關聯事件 {{eventId}}", + "attachment_picker_paste_label": "或貼上事件 ID", + "attachment_picker_attach": "關聯", + "attachment_picker_placeholder": "關聯一個事件", + "quick_reply_find_similar": "查詢相似抓拍事件", + "quick_reply_tell_me_more": "瞭解更多詳情", + "quick_reply_when_else": "還在哪些時段出現過?", + "quick_reply_find_similar_text": "查詢與此相似的抓拍記錄。", + "quick_reply_tell_me_more_text": "瞭解此條更多詳情。", + "quick_reply_when_else_text": "還在哪些時間出現過?", + "anchor": "來源", + "similarity_score": "相似度", + "no_similar_objects_found": "未找到相似目標。", + "semantic_search_required": "必須啟用語意搜尋才能查詢相似目標。", + "send": "傳送", + "suggested_requests": "嘗試問問:", + "starting_requests": { + "show_recent_events": "檢視近期事件", + "show_camera_status": "顯示攝影機狀態", + "recap": "我不在的時候發生了什麼?", + "watch_camera": "監控攝影機活動" + }, + "starting_requests_prompts": { + "show_recent_events": "顯示最近一小時的事件", + "show_camera_status": "我的攝影機當前狀態如何?", + "recap": "我不在的時候發生了什麼事?", + "watch_camera": "監控前門,有人出現就通知我" + }, + "new_chat": "新對話", + "settings": { + "title": "對話設定", + "show_stats": { + "title": "顯示統計", + "desc": "顯示對話回應的產生速度與上下文大小。", + "while_generating": "產生時", + "always": "一律顯示" + }, + "auto_scroll": { + "title": "自動捲動", + "desc": "隨新訊息到來自動跟進。" + } + }, + "stats": { + "context": "{{tokens}} 個 token", + "tokens_per_second": "{{rate}} tokens/秒" + } +} diff --git a/web/public/locales/zh-Hant/views/classificationModel.json b/web/public/locales/zh-Hant/views/classificationModel.json index 796495f691..6c0a1d9651 100644 --- a/web/public/locales/zh-Hant/views/classificationModel.json +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -8,7 +8,8 @@ "trainedModel": "訓練模型成功。", "trainingModel": "已開始模型訓練。", "updatedModel": "已更新模型配置", - "renamedCategory": "成功修改分類名稱為{{name}}" + "renamedCategory": "成功修改分類名稱為{{name}}", + "reclassifiedImage": "成功重新分類圖片" }, "error": { "deleteImageFailed": "刪除失敗:{{errorMessage}}", @@ -18,7 +19,8 @@ "trainingFailed": "模型訓練失敗。請至Frigate 日誌查看詳情。", "trainingFailedToStart": "模型訓練啟動失敗: {{errorMessage}}", "updateModelFailed": "模型更新失敗: {{errorMessage}}", - "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}" + "renameCategoryFailed": "類別重新命名失敗: {{errorMessage}}", + "reclassifyFailed": "重新分類圖片失敗:{{errorMessage}}" } }, "documentTitle": "分類模型", @@ -95,14 +97,80 @@ "namePlaceholder": "請輸入模型名稱...", "type": "類別", "typeState": "狀態", - "typeObject": "物件" + "typeObject": "物件", + "classificationTypeDesc": "子標籤會為目標標籤新增附加文字(例如:“人員:美團”)。屬性是可搜尋的元資料,獨立儲存在目標的元資訊中。", + "classificationSubLabel": "子標籤", + "classificationAttribute": "屬性", + "classes": "類別", + "states": "狀態", + "classesTip": "瞭解類別", + "classesStateDesc": "定義攝影機區域內可能出現的不同狀態。例如:車庫門的“開啟”和“關閉”。", + "classesObjectDesc": "定義用於分類偵測目標的不同類別。例如:人員分類中的“快遞員”、“居民”、“陌生人”。", + "classPlaceholder": "請輸入分類名稱……", + "errors": { + "nameRequired": "模型名稱為必填項", + "nameLength": "模型名稱長度不能超過 64 個字元", + "nameOnlyNumbers": "模型名稱不能僅包含數字", + "classRequired": "至少需要一個類別", + "classesUnique": "類別名稱必須唯一", + "noneNotAllowed": "不能建立“none”(無標籤)類別", + "stateRequiresTwoClasses": "狀態模型至少需要兩個類別", + "objectLabelRequired": "請選擇一個目標標籤", + "objectTypeRequired": "請選擇一個目標標籤" + } }, "steps": { - "chooseExamples": "選擇範本" + "chooseExamples": "選擇範本", + "nameAndDefine": "名稱與定義", + "stateArea": "狀態區域" + }, + "title": "建立新分類", + "step2": { + "description": "選擇攝影機,併為攝影機定義要監控的區域。模型將對這些區域的狀態進行分類。", + "cameras": "攝影機", + "selectCamera": "選擇攝影機", + "noCameras": "點選 + 符號新增攝影機", + "selectCameraPrompt": "從清單中選擇一個攝影機以定義其偵測區域" + }, + "step3": { + "selectImagesPrompt": "選擇所有屬於 {{className}} 的圖片", + "selectImagesDescription": "點選影像進行選擇,完成該類別後點選“繼續”。", + "allImagesRequired_other": "請對所有圖片進行分類。還有 {{count}} 張圖片需要分類。", + "generating": { + "title": "正在生成樣本圖片", + "description": "Frigate 正在從錄影中提取代表性圖片。這可能需要一些時間……" + }, + "training": { + "title": "正在訓練模型", + "description": "系統正在後臺訓練模型。你可以關閉此對話方塊,訓練完成後模型將自動開始執行。" + }, + "retryGenerate": "重新生成", + "noImages": "未生成樣本影像", + "classifying": "正在分類與訓練……", + "trainingStarted": "已開始模型訓練", + "modelCreated": "模型建立成功。請在“最近分類”頁面為缺失的狀態新增圖片,然後訓練模型。", + "errors": { + "noCameras": "未配置攝影機", + "noObjectLabel": "未選擇目標標籤", + "generateFailed": "示例生成失敗:{{error}}", + "generationFailed": "生成失敗,請重試。", + "classifyFailed": "圖片分類失敗:{{error}}" + }, + "generateSuccess": "樣本圖片生成成功", + "refreshExamples": "生成新示例", + "refreshConfirm": { + "title": "需要生成新示例?", + "description": "此操作將生成一組新的圖片,並清除所有選擇內容(包括之前的所有類別)。你需要為所有類別重新選擇示例。" + }, + "missingStatesWarning": { + "title": "缺失分類示例", + "description": "並非所有類別都有示例。可嘗試生成新示例以查詢缺失的類別,或繼續該步驟,之後透過 “最近分類” 頁面新增圖片。" + } } }, "menu": { - "states": "狀態" + "states": "狀態", + "objects": "目標" }, "noModels": { "object": { @@ -111,7 +179,13 @@ "buttonText": "建立物件模型" }, "state": { - "description": "建立自訂模型,用於監控和分類特定攝影機區域的狀態變化。" + "description": "建立自訂模型,用於監控和分類特定攝影機區域的狀態變化。", + "title": "尚未建立狀態分類模型", + "buttonText": "建立狀態模型" } - } + }, + "categorizeImageAs": "圖片分類為:", + "categorizeImage": "圖片分類", + "reclassifyImageAs": "重新分類圖片為:", + "reclassifyImage": "重新分類圖片" } diff --git a/web/public/locales/zh-Hant/views/events.json b/web/public/locales/zh-Hant/views/events.json index 7d5b4d28c8..bbe3f4bbae 100644 --- a/web/public/locales/zh-Hant/views/events.json +++ b/web/public/locales/zh-Hant/views/events.json @@ -14,7 +14,9 @@ "description": "僅當該攝影機啟用錄製功能時,才能為該攝影機建立審查項目。" } }, - "timeline": "時間線", + "timeline": { + "label": "時間線" + }, "timeline.aria": "選擇時間線", "events": { "label": "事件", @@ -24,7 +26,9 @@ "documentTitle": "審核 - Frigate", "allCameras": "所有鏡頭", "recordings": { - "documentTitle": "錄影 - Frigate" + "documentTitle": "錄影 - Frigate", + "invalidSharedLink": "由於解析錯誤,無法開啟帶時間戳的錄製連結。", + "invalidSharedCamera": "由於攝影機未知或未獲授權,無法開啟帶時間戳的錄製連結。" }, "calendarFilter": { "last24Hours": "過去 24 小時" @@ -63,5 +67,28 @@ "normalActivity": "正常", "needsReview": "待審核", "securityConcern": "安全隱憂", - "select_all": "全選" + "select_all": "全選", + "motionSearch": { + "menuItem": "畫面變動搜尋", + "openMenu": "攝影機選項" + }, + "motionPreviews": { + "menuItem": "檢視畫面變動預覽", + "title": "畫面變動預覽:{{camera}}", + "mobileSettingsTitle": "畫面變動預覽設定", + "mobileSettingsDesc": "調整播放速度和變暗程度,並選擇日期以僅檢視畫面變動的片段。", + "dim": "變暗", + "dimAria": "調整變暗強度", + "dimDesc": "增加變暗程度可以提高畫面變動區域的可見性。", + "speed": "速度", + "speedAria": "選擇預覽播放速度", + "speedDesc": "選擇預覽片段的播放速度。", + "back": "返回", + "empty": "沒有可用的預覽", + "noPreview": "預覽不可用", + "seekAria": "將 {{camera}} 播放器定位到 {{time}}", + "filter": "篩選", + "filterDesc": "選擇區域以僅顯示在這些區域中有畫面變動的片段。", + "filterClear": "清除" + } } diff --git a/web/public/locales/zh-Hant/views/explore.json b/web/public/locales/zh-Hant/views/explore.json index 5987009635..671e9201bf 100644 --- a/web/public/locales/zh-Hant/views/explore.json +++ b/web/public/locales/zh-Hant/views/explore.json @@ -112,7 +112,8 @@ "attributes": "分類屬性", "title": { "label": "標題" - } + }, + "scoreInfo": "分數資訊" }, "trackedObjectDetails": "追蹤物件詳情", "type": { @@ -221,12 +222,22 @@ "viewTrackingDetails": { "label": "檢視追蹤詳細資訊", "aria": "顯示追蹤詳細資訊" + }, + "debugReplay": { + "label": "除錯回放", + "aria": "在除錯回放檢視中檢視此被追蹤物件" + }, + "more": { + "aria": "更多" } }, "dialog": { "confirmDelete": { "title": "確認刪除", "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會被刪除。

    你確定要刪除嗎?" + }, + "toast": { + "error": "刪除該追蹤目標時出錯:{{errorMessage}}" } }, "noTrackedObjects": "找不到追蹤物件", @@ -268,7 +279,10 @@ "zones": "區域", "ratio": "比例", "score": "分數", - "area": "面積" + "area": "面積", + "computedScore": "計算得分", + "topScore": "最高得分", + "toggleAdvancedScores": "切換高階分數" } }, "annotationSettings": { @@ -294,5 +308,8 @@ }, "aiAnalysis": { "title": "AI 分析" + }, + "concerns": { + "label": "風險等級" } } diff --git a/web/public/locales/zh-Hant/views/exports.json b/web/public/locales/zh-Hant/views/exports.json index 3d3f9e87c6..0b376bcfd2 100644 --- a/web/public/locales/zh-Hant/views/exports.json +++ b/web/public/locales/zh-Hant/views/exports.json @@ -2,7 +2,9 @@ "search": "搜尋", "documentTitle": "匯出 - Frigate", "noExports": "找不到匯出內容", - "deleteExport": "刪除匯出內容", + "deleteExport": { + "label": "刪除匯出" + }, "editExport": { "saveExport": "儲存匯出內容", "title": "重新命名匯出內容", @@ -10,7 +12,10 @@ }, "toast": { "error": { - "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}" + "renameExportFailed": "重新命名匯出內容失敗:{{errorMessage}}", + "assignCaseFailed": "更新案件分配失敗:{{errorMessage}}", + "caseSaveFailed": "儲存案件失敗:{{errorMessage}}", + "caseDeleteFailed": "刪除案件失敗:{{errorMessage}}" } }, "deleteExport.desc": "你確定要刪除 {{exportName}} 嗎?", @@ -18,6 +23,106 @@ "shareExport": "分享匯出", "downloadVideo": "下載影片", "editName": "編輯名稱", - "deleteExport": "刪除匯出" + "deleteExport": "刪除匯出", + "assignToCase": "加入案件", + "removeFromCase": "從案件中移除" + }, + "headings": { + "cases": "案件", + "uncategorizedExports": "未分類匯出項" + }, + "toolbar": { + "newCase": "新案件", + "addExport": "新匯出", + "editCase": "編輯案件", + "deleteCase": "刪除案件" + }, + "deleteCase": { + "label": "刪除案件", + "desc": "你確定要刪除 {{caseName}} 嗎?", + "descKeepExports": "匯出檔案將繼續保留為未分類匯出。", + "descDeleteExports": "此案件中的所有匯出項都將被永久刪除。", + "deleteExports": "同時刪除匯出檔案" + }, + "caseDialog": { + "title": "加入案件", + "description": "選擇現有案件或建立新案件。", + "selectLabel": "案件", + "newCaseOption": "建立新案件", + "nameLabel": "案件名稱", + "descriptionLabel": "描述" + }, + "caseCard": { + "emptyCase": "暫無匯出檔案" + }, + "jobCard": { + "defaultName": "{{camera}} 匯出", + "queued": "佇列中", + "running": "執行中", + "preparing": "準備中", + "copying": "複製中", + "encoding": "編碼中", + "encodingRetry": "重試編碼中", + "finalizing": "正在完成" + }, + "caseView": { + "noDescription": "沒有描述", + "createdAt": "已建立 {{value}}", + "exportCount_one": "1 個匯出", + "exportCount_other": "{{count}} 個匯出", + "cameraCount_one": "1 個攝影機", + "cameraCount_other": "{{count}} 個攝影機", + "showMore": "顯示更多", + "showLess": "顯示更少", + "emptyTitle": "該案件為空", + "emptyDescription": "將現有未分類的匯出新增進來,以便整理該條目。", + "emptyDescriptionNoExports": "目前沒有可新增的未分類匯出項。" + }, + "caseEditor": { + "createTitle": "建立案件", + "editTitle": "編輯案件", + "namePlaceholder": "案件名稱", + "descriptionPlaceholder": "為該案件新增備註或相關說明" + }, + "addExportDialog": { + "title": "將匯出新增到 {{caseName}}", + "searchPlaceholder": "搜尋未分類的匯出項", + "empty": "未找到匹配的未分類匯出。", + "addButton_one": "新增 1 個匯出", + "addButton_other": "新增 {{count}} 個匯出", + "adding": "新增中…" + }, + "selected_one": "已選擇 {{count}} 個", + "selected_other": "已選擇 {{count}} 個", + "bulkActions": { + "addToCase": "新增至案件", + "moveToCase": "移動至案件", + "removeFromCase": "從案件中移除", + "delete": "刪除", + "deleteNow": "立即刪除" + }, + "bulkDelete": { + "title": "刪除匯出", + "desc_one": "你確定要刪除 {{count}} 個匯出嗎?", + "desc_other": "確定要刪除 {{count}} 個匯出嗎?" + }, + "bulkRemoveFromCase": { + "title": "從案件中移除", + "desc_one": "你確定要從該案件中移除這 {{count}} 個匯出嗎?", + "desc_other": "你確定要從該案件中移除這 {{count}} 個匯出嗎?", + "descKeepExports": "匯出將被移至未分類。", + "descDeleteExports": "匯出將被永久刪除。", + "deleteExports": "選擇刪除匯出" + }, + "bulkToast": { + "success": { + "delete": "已刪除匯出", + "reassign": "已更新案件分配", + "remove": "已從案件中移除匯出" + }, + "error": { + "deleteFailed": "刪除匯出失敗:{{errorMessage}}", + "reassignFailed": "更新案件分配失敗:{{errorMessage}}" + } } } diff --git a/web/public/locales/zh-Hant/views/faceLibrary.json b/web/public/locales/zh-Hant/views/faceLibrary.json index 938bf15818..496e1631db 100644 --- a/web/public/locales/zh-Hant/views/faceLibrary.json +++ b/web/public/locales/zh-Hant/views/faceLibrary.json @@ -2,7 +2,8 @@ "description": { "addFace": "上傳您的第一張照片至臉部資料庫以新增一個新的集合。", "placeholder": "輸入此集合的名稱", - "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。" + "invalidName": "無效的名稱。名稱只能包涵英數字、空格、撇(')、底線(_)及連字號(-)。", + "nameCannotContainHash": "名稱中不允許包含“#”符號。" }, "details": { "person": "人", @@ -38,7 +39,11 @@ "title": "最近的識別紀錄", "aria": "選擇最近的識別紀錄", "empty": "最近沒有辨識人臉的操作", - "titleShort": "最近" + "titleShort": "最近", + "emptyNoLibrary": { + "title": "上傳一張人臉", + "description": "您必須先在資料庫中加入至少一張人臉,才能使用人臉辨識功能。" + } }, "selectFace": "選擇人臉", "deleteFaceLibrary": { @@ -82,7 +87,8 @@ "deletedName_other": "{{count}} 個人臉已成功刪除。", "renamedFace": "成功將人臉重新命名為 {{name}}", "trainedFace": "成功訓練人臉。", - "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。" + "updatedFaceScore": "成功更新人臉分數{{name}}({{score}})。", + "reclassifiedFace": "重新分類人臉成功。" }, "error": { "uploadingImageFailed": "上傳圖片失敗:{{errorMessage}}", @@ -91,7 +97,10 @@ "deleteNameFailed": "刪除名稱失敗:{{errorMessage}}", "renameFaceFailed": "重新命名人臉失敗:{{errorMessage}}", "trainFailed": "訓練失敗:{{errorMessage}}", - "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}" + "updateFaceScoreFailed": "更新人臉分數失敗:{{errorMessage}}", + "reclassifyFailed": "重新分類人臉失敗:{{errorMessage}}" } - } + }, + "reclassifyFaceAs": "將人臉重新分類為:", + "reclassifyFace": "重新分類人臉" } diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json index a839b4b881..d1e28743fd 100644 --- a/web/public/locales/zh-Hant/views/live.json +++ b/web/public/locales/zh-Hant/views/live.json @@ -1,5 +1,7 @@ { - "documentTitle": "即時畫面 - Frigate", + "documentTitle": { + "default": "即時監控 - Frigate" + }, "documentTitle.withCamera": "{{camera}} - 即時畫面 - Frigate", "lowBandwidthMode": "低流量模式", "twoWayTalk": { @@ -11,7 +13,8 @@ "clickMove": { "label": "點擊畫面以置中鏡頭", "enable": "啟用點擊移動", - "disable": "停用點擊移動" + "disable": "停用點擊移動", + "enableWithZoom": "開啟點選移動 / 拖動縮放功能" }, "left": { "label": "向左移動 PTZ 鏡頭" @@ -67,7 +70,8 @@ }, "recording": { "enable": "啟用錄影", - "disable": "停用錄影" + "disable": "停用錄影", + "disabledInConfig": "必須先在該攝影機的設定中開啟錄製功能。" }, "snapshots": { "enable": "啟用截圖", @@ -134,6 +138,9 @@ "playInBackground": { "label": "背景播放", "tips": "啟用此選項以在播放器被隱藏時繼續播放串流。" + }, + "debug": { + "picker": "除錯模式下無法切換影片流。除錯將始終使用偵測(detect)功能的影片流。" } }, "cameraSettings": { @@ -143,7 +150,8 @@ "recording": "錄影", "snapshots": "截圖", "audioDetection": "音訊偵測", - "autotracking": "自動追蹤" + "autotracking": "自動追蹤", + "transcription": "音訊轉錄" }, "history": { "label": "顯示歷史影像" @@ -172,5 +180,24 @@ "noVideoSource": "沒有可用的影片資源以擷取快照。", "captureFailed": "快照擷取失敗。", "downloadStarted": "已開始下載快照。" + }, + "noCameras": { + "title": "未設定攝影機", + "description": "準備開始連線攝影機至 Frigate 。", + "buttonText": "新增攝影機", + "restricted": { + "title": "無可用攝影機", + "description": "你沒有權限檢視此分組中的任何攝影機。" + }, + "default": { + "title": "沒有配置攝影機", + "description": "現在就將攝影機接入到 Frigate 吧。", + "buttonText": "新增攝影機" + }, + "group": { + "title": "攝影機組目前為空", + "description": "該攝影機組未分配或啟動了攝影機。", + "buttonText": "管理攝影機組" + } } } diff --git a/web/public/locales/zh-Hant/views/motionSearch.json b/web/public/locales/zh-Hant/views/motionSearch.json index 0967ef424b..a83835afa0 100644 --- a/web/public/locales/zh-Hant/views/motionSearch.json +++ b/web/public/locales/zh-Hant/views/motionSearch.json @@ -1 +1,73 @@ -{} +{ + "documentTitle": "變動搜尋 - Frigate", + "title": "畫面變動搜尋", + "description": "繪製一個多邊形以劃定感興趣區域,並指定時間範圍,檢索該區域內的動態變化。", + "selectCamera": "畫面變動搜尋正在載入中", + "startSearch": "開始搜尋", + "searchStarted": "搜尋已開始", + "searchCancelled": "搜尋已取消", + "cancelSearch": "取消", + "searching": "搜尋進行中。", + "searchComplete": "搜尋完成", + "noResultsYet": "在所選區域內執行搜尋,查詢異常變化", + "noChangesFound": "所選區域未偵測到像素變化", + "changesFound_other": "偵測到 {{count}} 處畫面變化", + "framesProcessed": "已處理 {{count}} 幀畫面", + "jumpToTime": "跳轉到該時間", + "results": "結果", + "showSegmentHeatmap": "熱力圖", + "newSearch": "新的搜尋", + "clearResults": "清除結果", + "clearROI": "清除多邊形選區", + "polygonControls": { + "points_other": "{{count}} 個點位", + "undo": "撤銷上一個點位", + "reset": "重設多邊形" + }, + "motionHeatmapLabel": "畫面變動熱力圖", + "dialog": { + "title": "畫面變動搜尋", + "cameraLabel": "攝影機", + "previewAlt": "{{camera}} 攝影機即時預覽" + }, + "timeRange": { + "title": "搜尋範圍", + "start": "開始時間", + "end": "結束時間" + }, + "settings": { + "title": "搜尋設定", + "parallelMode": "並行模式", + "parallelModeDesc": "同時掃描多個錄製片段(速度更快,但 CPU 佔用會顯著升高)", + "threshold": "靈敏度閾值", + "thresholdDesc": "數值越低,可偵測到越小的變化(取值範圍 1-255)", + "minArea": "最小變化區域", + "minAreaDesc": "最小感興趣區域變化佔比,達到該比例才會判定為有效變動", + "frameSkip": "幀跳過", + "frameSkipDesc": "每隔 N 幀進行一次處理。將該值設定為攝影機的幀率,即可實現每秒處理一幀畫面(例如:5 幀 / 秒的攝影機設為 5,30 幀 / 秒的攝影機設為 30)。數值越高處理速度越快,但有可能遺漏短時移動偵測事件。", + "maxResults": "最大結果數", + "maxResultsDesc": "匹配到設定條數的錄影事件後,就自動停止檢索" + }, + "errors": { + "noCamera": "請選擇攝影機", + "noROI": "請繪製感興趣的區域", + "noTimeRange": "請選擇時間範圍", + "invalidTimeRange": "結束時間必須在開始時間之後", + "searchFailed": "搜尋失敗:{{message}}", + "polygonTooSmall": "多邊形至少需要 3 個頂點", + "unknown": "未知錯誤" + }, + "changePercentage": "{{percentage}}% 已變化", + "metrics": { + "title": "搜尋指標", + "segmentsScanned": "已掃描片段數", + "segmentsProcessed": "已處理", + "segmentsSkippedInactive": "已跳過(無活動)", + "segmentsSkippedHeatmap": "已跳過(不在感興趣區域)", + "fallbackFullRange": "備用全範圍掃描", + "framesDecoded": "畫面已解碼", + "wallTime": "搜尋時間", + "segmentErrors": "片段異常", + "seconds": "{{seconds}} 秒" + } +} diff --git a/web/public/locales/zh-Hant/views/replay.json b/web/public/locales/zh-Hant/views/replay.json index 0967ef424b..afe2cee4c6 100644 --- a/web/public/locales/zh-Hant/views/replay.json +++ b/web/public/locales/zh-Hant/views/replay.json @@ -1 +1,59 @@ -{} +{ + "title": "除錯回放", + "description": "回放攝影機錄影以供除錯。目標清單會延時展示已偵測目標的彙總資訊,訊息分頁則即時展示回放錄影對應的 Frigate 內部日誌資訊流。", + "websocket_messages": "訊息", + "dialog": { + "title": "開始除錯回放", + "description": "建立臨時回放攝影機,迴圈播放歷史錄製影片,用於除錯目標偵測與追蹤相關問題。臨時回放的攝影機將沿用原攝影機的偵測配置。請選擇一個時間範圍開始。", + "camera": "原攝影機", + "timeRange": "時間範圍", + "preset": { + "1m": "最後 1 分鐘", + "5m": "最後 5 分鐘", + "timeline": "從時間線", + "custom": "自訂" + }, + "startButton": "開始回放", + "selectFromTimeline": "選擇", + "starting": "開始回放…", + "startLabel": "開始", + "endLabel": "結束", + "toast": { + "error": "除錯回放啟動失敗:{{error}}", + "alreadyActive": "已有回放工作階段正在執行", + "stopError": "除錯回放停止失敗:{{error}}", + "goToReplay": "進入回放" + } + }, + "page": { + "noSession": "沒有正在進行的除錯回放工作階段", + "noSessionDesc": "從歷史回放頁面啟動除錯回放:點選工具列中的操作按鈕,選擇除錯回放即可。", + "goToRecordings": "檢視歷史記錄", + "preparingClip": "正在準備片段…", + "preparingClipDesc": "Frigate 正在拼接所選時間範圍的錄影片段。時間跨度較大時,該過程可能需要一分鐘左右。", + "startingCamera": "開始除錯回放中…", + "startError": { + "title": "除錯回放啟動失敗", + "back": "返回歷史記錄" + }, + "sourceCamera": "源攝影機", + "replayCamera": "回放攝影機", + "initializingReplay": "初始化除錯回放中…", + "stoppingReplay": "正在停止除錯回放…", + "stopReplay": "停止回放", + "confirmStop": { + "title": "要停止除錯回放嗎?", + "description": "這將終止工作階段並清除所有臨時資料。是否確定?", + "confirm": "停止回放", + "cancel": "取消" + }, + "activity": "活動", + "objects": "目標清單", + "audioDetections": "音訊偵測", + "noActivity": "未偵測到活動", + "activeTracking": "活動追蹤中", + "noActiveTracking": "沒有活動追蹤", + "configuration": "配置", + "configurationDesc": "微調除錯回放攝影機的移動偵測與目標追蹤引數。本次調整不會儲存到你的 Frigate 設定檔中。" + } +} diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json index 97829f5360..5252467276 100644 --- a/web/public/locales/zh-Hant/views/settings.json +++ b/web/public/locales/zh-Hant/views/settings.json @@ -11,7 +11,12 @@ "motionTuner": "移動偵測調教器 - Frigate", "object": "除錯 - Frigate", "cameraManagement": "管理鏡頭 - Frigate", - "cameraReview": "相機預覽設置 - Frigate" + "cameraReview": "相機預覽設置 - Frigate", + "globalConfig": "全域性配置 - Frigate", + "cameraConfig": "攝影機配置 - Frigate", + "detectorsAndModel": "偵測器與模型 - Frigate", + "maintenance": "維護 - Frigate", + "profiles": "設定檔 - Frigate" }, "menu": { "ui": "使用者介面", @@ -26,7 +31,65 @@ "triggers": "觸發", "cameraManagement": "管理", "cameraReview": "預覽", - "roles": "角色" + "roles": "角色", + "general": "常規", + "globalConfig": "全域性配置", + "system": "系統", + "integrations": "整合", + "uiSettings": "介面設定", + "profiles": "設定檔", + "globalDetect": "目標偵測", + "globalRecording": "錄製", + "globalSnapshots": "快照", + "globalFfmpeg": "FFmpeg", + "globalMotion": "畫面變動偵測", + "globalObjects": "目標", + "globalReview": "審閱", + "globalAudioEvents": "音訊偵測", + "globalLivePlayback": "即時監控觀看", + "globalTimestampStyle": "時間戳樣式", + "systemDatabase": "資料庫", + "systemTls": "TLS加密連結", + "systemAuthentication": "驗證", + "systemNetworking": "網路", + "systemProxy": "代理", + "systemUi": "介面", + "systemLogging": "日誌", + "systemEnvironmentVariables": "環境變數", + "systemTelemetry": "遙測", + "systemBirdseye": "鳥瞰圖", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "偵測器與模型", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "go2rtc 影片流", + "integrationSemanticSearch": "語意搜尋", + "integrationGenerativeAi": "生成式 AI", + "integrationFaceRecognition": "人臉辨識", + "integrationLpr": "車牌辨識", + "integrationObjectClassification": "目標分類", + "integrationAudioTranscription": "音訊轉錄", + "cameraDetect": "目標偵測", + "cameraFfmpeg": "FFmpeg", + "cameraRecording": "錄製", + "cameraSnapshots": "快照", + "cameraMotion": "畫面變動偵測", + "cameraObjects": "目標", + "cameraConfigReview": "審閱", + "cameraAudioEvents": "音訊偵測", + "cameraAudioTranscription": "音訊轉錄", + "cameraNotifications": "通知", + "cameraLivePlayback": "即時監控觀看", + "cameraBirdseye": "鳥瞰圖", + "cameraFaceRecognition": "人臉辨識", + "cameraLpr": "車牌辨識", + "cameraMqttConfig": "MQTT", + "cameraOnvif": "ONVIF", + "cameraUi": "攝影機頁面", + "cameraTimestampStyle": "時間戳樣式", + "cameraMqtt": "攝影機 MQTT", + "maintenance": "維護", + "mediaSync": "媒體同步", + "regionGrid": "區域網格" }, "dialog": { "unsavedChanges": { @@ -103,22 +166,56 @@ "modelSize": { "label": "模型大小", "small": { - "title": "小" + "title": "小", + "desc": "將使用 模型。該模型使用的記憶體較少,在 CPU 上也能較快的執行,品質較好。" + }, + "desc": "用於語意搜尋的語言模型大小。", + "large": { + "title": "大", + "desc": "將使用 模型。該選項使用了完整的 Jina 模型,條件允許的情況下將自動使用 GPU 執行。" } }, "title": "語意搜尋", "desc": "Frigate 中的語意搜尋功能可讓您使用圖像本身、使用者定義的文字描述或自動產生的描述,在審核專案中尋找追蹤物件。", "reindexNow": { "label": "立即重新索引", - "desc": "重新索引會為所有追蹤物件重新產生嵌入向量。此過程在背景運行,可能會佔用大量 CPU 資源,並且耗時較長,具體取決於追蹤物件的數量。" + "desc": "重新索引會為所有追蹤物件重新產生嵌入向量。此過程在背景運行,可能會佔用大量 CPU 資源,並且耗時較長,具體取決於追蹤物件的數量。", + "confirmTitle": "確認重建索引", + "confirmDesc": "確定要為所有追蹤目標重建特徵向量索引資訊嗎?此過程將在後臺進行,但可能會導致CPU滿載並耗費較長時間。您可以在 瀏覽 頁面檢視進度。", + "confirmButton": "重建索引", + "success": "重建索引已成功啟動。", + "alreadyInProgress": "重建索引已在執行中。", + "error": "啟動重建索引失敗:{{errorMessage}}" } }, "faceRecognition": { - "title": "人臉識別" + "title": "人臉識別", + "desc": "人臉辨識功能允許為人物分配名稱,當辨識到他們的面孔時,Frigate 會將人物的名字作為子標籤進行分配。這些資訊會顯示在介面、過濾器以及通知中。", + "modelSize": { + "label": "模型大小", + "desc": "用於人臉辨識的模型大小。", + "small": { + "title": "小", + "desc": "將使用模型。該選項採用 FaceNet 人臉特徵提取模型,可在大多數 CPU 上高效執行。" + }, + "large": { + "title": "大", + "desc": "將使用模型。該選項使用 ArcFace 人臉特徵提取模型,條件允許的情況下將自動使用 GPU 執行。" + } + } }, "birdClassification": { "title": "鳥類分類", "desc": "鳥類分類功能使用量化的 TensorFlow 模型識別已知鳥類。識別出已知鳥類後,其通用名稱將作為子標籤添加。此資訊會顯示在使用者介面、篩選器以及通知中。" + }, + "licensePlateRecognition": { + "title": "車牌辨識", + "desc": "Frigate 可以辨識車輛的車牌,並自動將偵測到的字元新增到 辨識的車牌(recognized_license_plate)欄位中,或將已知車牌對應的名稱作為子標籤新增到該車輛目標中。該功能常用於辨識駛入車道的車輛車牌或經過街道的車輛車牌。" + }, + "restart_required": "需要重啟(增強功能設定已儲存)", + "toast": { + "success": "增強功能設定已儲存。請重啟 Frigate 以應用更改。", + "error": "配置更改儲存失敗:{{errorMessage}}" } }, "cameraWizard": { @@ -126,10 +223,12 @@ "testResultLabels": { "resolution": "解析度", "video": "影像", - "audio": "語音" + "audio": "語音", + "fps": "幀率" }, "commonErrors": { - "testFailed": "串流測試失敗: {{error}}" + "testFailed": "串流測試失敗: {{error}}", + "noUrl": "請提供正確的影片流地址" }, "step1": { "description": "輸入相機詳細資訊並選擇自動偵測或手動選擇相機品牌。", @@ -142,15 +241,1539 @@ "password": "密碼", "passwordPlaceholder": "選填", "selectTransport": "選擇協議", - "cameraBrand": "相機品牌" + "cameraBrand": "相機品牌", + "selectBrand": "選擇攝影機品牌用於生成URL地址模板", + "customUrl": "自訂影片流地址", + "brandInformation": "品牌資訊", + "brandUrlFormat": "對於採用RTSP URL格式的攝影機,其格式為:{{exampleUrl}}", + "customUrlPlaceholder": "rtsp://使用者名稱:密碼@主機或IP地址:埠/路徑", + "connectionSettings": "連線設定", + "detectionMethod": "影片流偵測方法", + "onvifPort": "ONVIF 埠", + "probeMode": "探測攝影機", + "manualMode": "手動選擇", + "detectionMethodDescription": "如果攝影機支援 ONVIF 協議,將使用該協議探測攝影機,以自動獲取攝影機影片流地址;若不支援,也可手動選擇攝影機品牌來使用預設地址。如需輸入自訂RTSP地址,請選擇“手動選擇”並選擇“其他”選項。", + "onvifPortDescription": "對於支援ONVIF協議的攝影機,該埠通常為80或8080。", + "useDigestAuth": "使用摘要認證", + "useDigestAuthDescription": "為 ONVIF 協議啟用 HTTP 摘要認證。部分攝影機可能需要專用的 ONVIF 使用者名稱/密碼,而非預設的 admin 帳戶。", + "errors": { + "brandOrCustomUrlRequired": "請選擇攝影機品牌並配置主機/ IP 地址,或選擇“其他”後手動配置影片流地址", + "nameRequired": "攝影機名稱為必填項", + "nameLength": "攝影機名稱要少於64個字元", + "invalidCharacters": "攝影機名稱內有不允許使用的字元", + "nameExists": "該攝影機名稱已存在", + "customUrlRtspRequired": "自訂 URL 必須以“rtsp://”開頭;對於非 RTSP 協議的攝影機流,需手動新增至設定檔。" + } + }, + "description": "請按照以下步驟新增攝影機至 Frigate 中。", + "steps": { + "nameAndConnection": "名稱與連線", + "probeOrSnapshot": "探測或快照", + "streamConfiguration": "影片流配置", + "validationAndTesting": "驗證與測試" + }, + "save": { + "success": "已儲存新攝影機 {{cameraName}}。", + "failure": "儲存攝影機 {{cameraName}} 遇到了錯誤。" + }, + "step2": { + "description": "將根據你選擇的偵測方式,將會自動查詢攝影機可用流配置,或進行手動配置。", + "testSuccess": "影片流測試成功!", + "testFailed": "連線測試失敗,請檢查您的輸入後重試。", + "testFailedTitle": "測試失敗", + "streamDetails": "影片流詳情", + "probing": "正在偵測攝影機中……", + "retry": "重試", + "testing": { + "probingMetadata": "正在查詢攝影機引數……", + "fetchingSnapshot": "正在獲取攝影機快照……" + }, + "probeFailed": "偵測攝影機失敗:{{error}}", + "probingDevice": "尋找裝置中……", + "probeSuccessful": "偵測成功", + "probeError": "偵測遇到錯誤", + "probeNoSuccess": "偵測未成功", + "deviceInfo": "裝置資訊", + "manufacturer": "製造商", + "model": "型號", + "firmware": "韌體", + "profiles": "設定檔", + "ptzSupport": "支援 PTZ", + "autotrackingSupport": "支援自動追蹤", + "presets": "預設配置", + "rtspCandidates": "RTSP候選地址", + "rtspCandidatesDescription": "透過攝影機自動偵測發現了以下RTSP地址。測試連線以檢視影片流引數。", + "noRtspCandidates": "未從攝影機偵測到任何 RTSP 地址。可能是你的帳號密碼錯誤,或者攝影機不支援 ONVIF 協議,亦或是當前採用的 RTSP 地址獲取方式無效。請返回上一步,嘗試手動輸入RTSP地址。", + "candidateStreamTitle": "候選{{number}}", + "useCandidate": "使用", + "uriCopy": "複製", + "uriCopied": "地址已複製到剪貼簿", + "testConnection": "測試連線", + "toggleUriView": "點選切換完整 URI 顯示", + "connected": "已連線", + "notConnected": "未連線", + "errors": { + "hostRequired": "主機/IP地址為必填" + } + }, + "step3": { + "description": "為你的攝影機配置影片流功能並新增額外影片流。", + "streamsTitle": "攝影機影片流", + "addStream": "新增影片流", + "addAnotherStream": "新增其他影片流", + "streamTitle": "{{number}} 號影片流", + "streamUrl": "影片流地址", + "streamUrlPlaceholder": "rtsp://使用者名稱:密碼@主機:埠/路徑", + "selectStream": "選擇一個影片流", + "searchCandidates": "搜尋候選項……", + "noStreamFound": "沒有找到影片流", + "url": "URL地址", + "resolution": "解析度", + "selectResolution": "選擇解析度", + "quality": "品質", + "selectQuality": "選擇品質", + "roles": "功能", + "roleLabels": { + "detect": "目標偵測", + "record": "錄製", + "audio": "音訊偵測" + }, + "testStream": "測試連線", + "testSuccess": "影片流測試成功!", + "testFailed": "影片流測試失敗", + "testFailedTitle": "測試失敗", + "connected": "已連線", + "notConnected": "未連線", + "featuresTitle": "功能特性", + "go2rtc": "減少與攝影機的連線數", + "detectRoleWarning": "必須得有一個影片流設定了“偵測”功能才能繼續操作。", + "rolesPopover": { + "title": "影片流功能", + "detect": "用於目標偵測的主碼流。", + "record": "根據配置設定儲存影片流片段。", + "audio": "用於音訊偵測的音影片流。" + }, + "featuresPopover": { + "title": "影片流功能特性", + "description": "使用 go2rtc 中繼轉流功能,減少與攝影機的網路連線數,提升效率。" + } + }, + "step4": { + "description": "將進行儲存新攝影機配置前的最終驗證與分析,請在儲存前確保所有影片流均已連線。", + "validationTitle": "影片流驗證", + "connectAllStreams": "連線所有影片流", + "reconnectionSuccess": "重新連線成功。", + "reconnectionPartial": "部分影片流重新連線失敗。", + "streamUnavailable": "影片流預覽不可用", + "reload": "重新載入", + "connecting": "連線中……", + "streamTitle": "影片流 {{number}}", + "valid": "透過", + "failed": "失敗", + "notTested": "未測試", + "connectStream": "連線", + "connectingStream": "連線中", + "disconnectStream": "斷開連線", + "estimatedBandwidth": "預估頻寬", + "roles": "功能", + "ffmpegModule": "使用影片流相容模式", + "ffmpegModuleDescription": "若多次嘗試後仍無法載入影片流,可嘗試啟用此功能。啟用後,Frigate 將透過 go2rtc 呼叫 ffmpeg 模組。這可能會提升與部分攝影機影片流的相容性。", + "none": "無", + "error": "錯誤", + "streamValidated": "影片流 {{number}} 驗證成功", + "streamValidationFailed": "影片流 {{number}} 驗證失敗", + "saveAndApply": "儲存新攝影機", + "saveError": "配置無效,請檢查您的設定。", + "issues": { + "title": "影片流驗證", + "videoCodecGood": "影片編解碼器為 {{codec}}。", + "audioCodecGood": "音訊編解碼器為 {{codec}}。", + "resolutionHigh": "使用 {{resolution}} 解析度可能導致資源使用率增加。", + "resolutionLow": "{{resolution}} 解析度可能過低,難以可靠偵測小型目標或物體。", + "resolutionUnknown": "無法偵測此影片流的解析度。你需要在設定或設定檔中手動指定偵測解析度。", + "noAudioWarning": "偵測到該影片流無音訊訊號,錄製影片將沒有聲音。", + "audioCodecRecordError": "錄製功能需要 AAC 音訊編解碼器以實現音訊支援。", + "audioCodecRequired": "要實現音訊偵測功能,必須要有音訊流。", + "restreamingWarning": "為錄製流開啟“減少與攝影機的連線數”可能會略微增加 CPU 使用率。", + "brands": { + "reolink-rtsp": "不建議使用 Reolink 的 RTSP 協議。請在攝影機後臺設定中啟用 HTTP協議,並重新啟動向導。", + "reolink-http": "Reolink HTTP 影片流應該使用 FFmpeg 以獲得更好的相容性,為此影片流啟用“使用流相容模式”。" + }, + "dahua": { + "substreamWarning": "子碼流1當前被鎖定為低解析度。多數大華、安訊士、EmpireTech品牌的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。" + }, + "hikvision": { + "substreamWarning": "子碼流1當前被鎖定為低解析度。多數海康威視的攝影機都支援額外的子碼流,這些子碼流需要在攝影機設定中手動啟用。如果你的裝置支援,建議你檢查並使用這些高解析度子碼流。" + } + } } }, "triggers": { "toast": { "error": { "deleteTriggerFailed": "刪除觸發器失敗:{{errorMessage}}", - "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}" + "updateTriggerFailed": "更新觸發器失敗:{{errorMessage}}", + "createTriggerFailed": "建立觸發器失敗:{{errorMessage}}" + }, + "success": { + "createTrigger": "觸發器 {{name}} 建立成功。", + "updateTrigger": "觸發器 {{name}} 更新成功。", + "deleteTrigger": "觸發器 {{name}} 已刪除。" } + }, + "documentTitle": "觸發器", + "semanticSearch": { + "title": "語意搜尋已關閉", + "desc": "必須啟用語意搜尋功能才能使用觸發器。" + }, + "management": { + "title": "觸發器", + "desc": "管理 {{camera}} 的觸發器。你可以選擇“縮圖”型別,將透過與追蹤目標相似的縮圖來觸發;也可以透過“描述”型別,與你描述的文字相似來觸發(中文描述需要使用 jina v2模型,對配置要求更高)。" + }, + "addTrigger": "新增觸發器", + "table": { + "name": "名稱", + "type": "型別", + "content": "觸發內容", + "threshold": "閾值", + "actions": "動作", + "noTriggers": "此攝影機未配置任何觸發器。", + "edit": "編輯", + "deleteTrigger": "刪除觸發器", + "lastTriggered": "最後一個觸發項" + }, + "type": { + "thumbnail": "縮圖", + "description": "描述" + }, + "actions": { + "notification": "傳送通知", + "sub_label": "新增子標籤", + "attribute": "新增屬性" + }, + "dialog": { + "createTrigger": { + "title": "建立觸發器", + "desc": "為攝影機 {{camera}} 建立觸發器" + }, + "editTrigger": { + "title": "編輯觸發器", + "desc": "編輯攝影機 {{camera}} 的觸發器設定" + }, + "deleteTrigger": { + "title": "刪除觸發器", + "desc": "你確定要刪除觸發器 {{triggerName}} 嗎?此操作不可撤銷。" + }, + "form": { + "name": { + "title": "名稱", + "placeholder": "觸發器名稱", + "description": "請輸入用於辨識此觸發器的唯一名稱或描述", + "error": { + "minLength": "該欄位至少需要兩個字元。", + "invalidCharacters": "該欄位只能包含字母、數字、下劃線和連字元。", + "alreadyExists": "此攝影機已存在同名觸發器。" + } + }, + "enabled": { + "description": "開啟/關閉此觸發器" + }, + "type": { + "title": "型別", + "placeholder": "選擇觸發型別", + "description": "當偵測到相似的追蹤目標描述時觸發", + "thumbnail": "當偵測到相似的追蹤目標縮圖時觸發" + }, + "content": { + "title": "內容", + "imagePlaceholder": "選擇圖片", + "textPlaceholder": "輸入文字內容", + "imageDesc": "僅顯示最近的 100 張縮圖。如果找不到需要的圖片,請前往“瀏覽”頁面檢視更早的目標,並從選單中設定觸發器。", + "textDesc": "輸入文字,當偵測到相似的追蹤目標描述時觸發此操作。", + "error": { + "required": "內容為必填項。" + } + }, + "threshold": { + "title": "閾值", + "desc": "設定此觸發器的相似度閾值。閾值越高,觸發所需的匹配就越精確。", + "error": { + "min": "閾值必須大於 0", + "max": "閾值必須小於 1" + } + }, + "actions": { + "title": "動作", + "desc": "預設情況下,Frigate 會為所有觸發器傳送 MQTT 訊息。子標籤會將觸發器名稱新增到目標標籤中。屬性是可搜尋的元資料,獨立儲存在追蹤目標的元資料中。", + "error": { + "min": "必須至少選擇一項動作。" + } + } + } + }, + "wizard": { + "title": "建立觸發器", + "step1": { + "description": "配置觸發器的基礎設定。" + }, + "step2": { + "description": "設定觸發此操作的內容。" + }, + "step3": { + "description": "配置此觸發器的相似度閾值與執行動作。" + }, + "steps": { + "nameAndType": "名稱與型別", + "configureData": "配置資料", + "thresholdAndActions": "閾值與動作" + } + } + }, + "button": { + "overriddenGlobal": "已覆蓋全域性通用配置", + "overriddenGlobalTooltip": "當前攝影機配置,將優先覆蓋全域性通用設定", + "overriddenGlobalHeading_other": "此攝影機覆蓋了全域性設定中的 {{count}} 個欄位:", + "overriddenGlobalNoDeltas": "此攝影機覆蓋了全域性設定,但所有欄位值都相同。", + "overriddenBaseConfig": "已覆蓋預設配置", + "overriddenBaseConfigTooltip": "當前 {{profile}} 設定檔會覆蓋本節所有設定", + "overriddenBaseConfigHeading_other": "{{profile}} 設定檔覆蓋了基礎設定中的 {{count}} 個欄位:", + "overriddenBaseConfigNoDeltas": "{{profile}} 設定檔覆蓋了此區段,但所有欄位值與基礎設定相同。", + "overriddenInCameras": { + "label_other": "已在 {{count}} 個攝影機中單獨配置", + "tooltip_other": "{{count}} 個攝影機在此項中存在單獨配置,點選檢視詳情。", + "heading_other": "此全域性設定項下有 {{count}} 個攝影機存在自訂單獨配置。", + "othersField_other": "其餘 {{count}} 個", + "profilePrefix": "{{profile}} 配置方案:{{fields}}" + } + }, + "saveAllPreview": { + "title": "未儲存的更改", + "triggerLabel": "檢視待處理的更改", + "empty": "沒有待處理的更改。", + "scope": { + "label": "作用範圍", + "global": "全域性", + "camera": "攝影機:{{cameraName}}" + }, + "profile": { + "label": "配置" + }, + "field": { + "label": "欄位" + }, + "value": { + "label": "新值", + "reset": "重設" + } + }, + "cameraManagement": { + "title": "管理攝影機", + "description": "新增、編輯和刪除攝影機,控制哪些攝影機已啟用,並設定按設定檔與攝影機類型的覆蓋。若要設定串流、偵測、動作及其他攝影機特定設定,請在「攝影機設定」下選擇對應的區段。", + "addCamera": "新增新攝影機", + "deleteCamera": "刪除攝影機", + "deleteCameraDialog": { + "title": "刪除攝影機", + "description": "刪除攝影機將永久移除該攝影機的所有錄影、跟蹤目標以及配置。任何與該攝影機關聯的 go2rtc 流可能仍需手動刪除。", + "selectPlaceholder": "選擇攝影機…", + "confirmTitle": "你確定嗎?", + "confirmWarning": "刪除 {{cameraName}} 後將無法撤銷。", + "deleteExports": "同時刪除該攝影機匯出的影片", + "confirmButton": "永久刪除", + "success": "攝影機 {{cameraName}} 刪除完成", + "error": "刪除攝影機 {{cameraName}} 失敗" + }, + "editCamera": "編輯攝影機:", + "selectCamera": "選擇攝影機", + "backToSettings": "返回攝影機設定", + "streams": { + "title": "開啟或關閉攝影機", + "enableLabel": "開啟攝影機", + "enableDesc": "暫時停用已開啟的攝影機,直到 Frigate 重啟。停用攝影機會完全停止 Frigate 對該攝影機影片流的處理。偵測、錄影和除錯功能將不可用。
    注意:這不會停用 go2rtc 的轉推流。", + "disableLabel": "關閉攝影機", + "disableDesc": "開啟在當前在介面中不可見且在配置中被停用的攝影機。啟用後需要重啟 Frigate 才能生效。", + "enableSuccess": "已在配置中啟用 {{cameraName}}。請重啟 Frigate 以應用更改。", + "friendlyName": { + "edit": "修改攝影機顯示名稱", + "title": "修改顯示名稱", + "description": "設定該攝像機在 Frigate 使用者介面中顯示的名稱。若留空,則使用攝像機 ID。", + "rename": "重新命名" + } + }, + "cameraConfig": { + "add": "新增攝影機", + "edit": "編輯攝影機", + "description": "配置攝影機設定,包括影片流輸入和功能選擇。", + "name": "攝影機名稱", + "nameRequired": "攝影機名稱為必填項", + "nameLength": "攝影機名稱必須少於64個字元。", + "namePlaceholder": "例如:大門、後院等", + "enabled": "開啟", + "ffmpeg": { + "inputs": "影片流輸入", + "path": "影片流地址", + "pathRequired": "影片流地址為必填項", + "pathPlaceholder": "rtsp://...", + "roles": "功能", + "rolesRequired": "至少選擇一個功能", + "rolesUnique": "每個功能(音訊audio、偵測detect、錄製record)只能分配給一個影片流", + "addInput": "新增輸入影片流", + "removeInput": "移除輸入影片流", + "inputsRequired": "至少需要一個輸入影片流" + }, + "go2rtcStreams": "go2rtc 影片流", + "streamUrls": "影片流地址", + "addUrl": "新增地址", + "addGo2rtcStream": "新增 go2rtc 影片流", + "toast": { + "success": "攝影機 {{cameraName}} 已儲存" + } + }, + "profiles": { + "title": "設定檔的攝影機覆蓋項", + "selectLabel": "選擇設定檔", + "description": "配置在啟用某個設定檔時,哪些攝影機應被開啟或關閉。設定為“繼承”的攝影機會沿用它原本的啟用/停用狀態。", + "inherit": "繼承", + "enabled": "開啟", + "disabled": "關閉" + }, + "cameraType": { + "title": "攝影機型別", + "label": "攝影機型別", + "description": "為每路攝影機設定型別。專用車牌辨識(LPR)攝影機為單用途裝置,配備高倍光學變焦,可抓拍遠處車輛的車牌。絕大多數攝影機應選用通用型別;只有專為車牌辨識部署、且畫面聚焦對準車牌的攝影機,才需選擇專用 LPR 型別。", + "normal": "通用", + "dedicatedLpr": "車牌辨識專用", + "saveSuccess": "已更新 {{cameraName}} 的攝影機型別,請重啟 Frigate 以使更改生效。" + } + }, + "cameraReview": { + "title": "攝影機審閱設定", + "object_descriptions": { + "title": "生成式AI目標描述", + "desc": "臨時啟用或停用此攝影機的 生成式AI目標描述 功能,直到 Frigate 重啟。停用後,系統將不再請求該攝影機追蹤目標和物體的AI生成描述。" + }, + "review_descriptions": { + "title": "生成式 AI 審閱總結", + "desc": "臨時開關該攝影機的 生成式 AI 審閱總結 功能,直到 Frigate 重啟。停用後,系統將不再請求 AI 生成該攝影機審閱項目的總結。" + }, + "review": { + "title": "審閱", + "desc": "臨時開關該攝影機的警報與偵測項生成功能,直到 Frigate 重啟後恢復。停用期間,系統將不再生成新的審閱項目。 ", + "alerts": "警報 ", + "detections": "偵測 " + }, + "reviewClassification": { + "title": "審閱分類", + "desc": "Frigate 將審閱項的嚴重程度分為“警報”和“偵測”兩個等級。預設情況下,所有的汽車 目標都將視為警報。你可以透過修改設定檔配置區域來細分。", + "noDefinedZones": "此攝影機未設定任何監控區。", + "objectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下都將視為警報。", + "zoneObjectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下的 {{zone}} 區域內都將視為警報。", + "objectDetectionsTips": "所有在攝影機 {{cameraName}} 上,偵測到的 {{detectionsLabels}} 目標或物體,無論它位於哪個區,都將顯示為偵測。", + "zoneObjectDetectionsTips": { + "text": "所有在攝影機 {{cameraName}} 下的 {{zone}} 區域內偵測到未分類的 {{detectionsLabels}} 目標或物體,都將顯示為偵測。", + "notSelectDetections": "所有在攝影機 {{cameraName}}下的 {{zone}} 區域內偵測到的 {{detectionsLabels}} 目標或物體,如果它未歸類為警報,無論它位於哪個區,都將顯示為偵測。", + "regardlessOfZoneObjectDetectionsTips": "在攝影機 {{cameraName}} 上,所有未分類的 {{detectionsLabels}} 偵測目標或物體,無論出現在哪個區域,都將顯示為偵測。" + }, + "unsavedChanges": "攝影機 {{camera}} 的審閱分類設定尚未儲存", + "selectAlertsZones": "選擇警報區", + "selectDetectionsZones": "選擇偵測區", + "limitDetections": "限制僅在特定區域內進行偵測", + "toast": { + "success": "審閱分類設定已儲存,重啟後生效。" + } + } + }, + "masksAndZones": { + "filter": { + "all": "所有遮罩和區域" + }, + "restart_required": "需要重啟(遮罩與區域已修改)", + "disabledInConfig": "該項目已在設定檔中被停用", + "addDisabledProfile": "先新增到基礎配置中,然後在設定檔中進行覆蓋", + "profileBase": "(基礎)", + "profileOverride": "(覆蓋)", + "toast": { + "success": { + "copyCoordinates": "已複製 {{polyName}} 的座標到剪貼簿。" + }, + "error": { + "copyCoordinatesFailed": "無法複製座標到剪貼簿。" + } + }, + "motionMaskLabel": "畫面變動遮罩 {{number}}", + "objectMaskLabel": "目標/物體遮罩 {{number}}", + "form": { + "id": { + "error": { + "mustNotBeEmpty": "ID 不能為空。", + "alreadyExists": "此攝影機已存在使用該 ID 的遮罩。" + } + }, + "name": { + "error": { + "mustNotBeEmpty": "名稱不能為空。" + } + }, + "zoneName": { + "error": { + "mustBeAtLeastTwoCharacters": "區域名稱必須至少包含 2 個字元。", + "mustNotBeSameWithCamera": "區域名稱不能與攝影機名稱相同。", + "alreadyExists": "該攝影機已有相同的區域名稱。", + "mustNotContainPeriod": "區域名稱不能包含句點。", + "hasIllegalCharacter": "區域名稱包含非法字元。", + "mustHaveAtLeastOneLetter": "區域名稱必須至少包含一個字母。" + } + }, + "distance": { + "error": { + "text": "距離必須大於或等於 0.1。", + "mustBeFilled": "所有距離欄位必須填寫才能使用速度估算。" + } + }, + "inertia": { + "error": { + "mustBeAboveZero": "慣性必須大於 0。" + } + }, + "loiteringTime": { + "error": { + "mustBeGreaterOrEqualZero": "徘徊時間必須大於或等於 0。" + } + }, + "speed": { + "error": { + "mustBeGreaterOrEqualTo": "速度閾值必須大於或等於0.1。" + } + }, + "polygonDrawing": { + "type": { + "zone": "區域", + "motion_mask": "畫面變動遮罩", + "object_mask": "目標遮罩" + }, + "removeLastPoint": "刪除最後一個點", + "reset": { + "label": "清除所有點" + }, + "snapPoints": { + "true": "啟用點對齊", + "false": "停用點對齊" + }, + "delete": { + "title": "確認刪除", + "desc": "你確定要刪除{{type}} “{{name}}” 嗎?", + "success": "{{name}} 已被刪除。" + }, + "revertOverride": { + "title": "恢復為預設配置", + "desc": "這將移除針對 {{type}} {{name}} 的配置覆蓋,並恢復為基礎配置。" + }, + "error": { + "mustBeFinished": "多邊形繪製必須完成閉合後才能儲存。" + } + } + }, + "zones": { + "label": "區域", + "documentTitle": "編輯區域 - Frigate", + "desc": { + "title": "該功能允許你定義特定區域,以便你可以確定特定目標或物體是否在該區域內。", + "documentation": "文件" + }, + "add": "新增區域", + "edit": "編輯區域", + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "區域名稱", + "inputPlaceHolder": "請輸入名稱…", + "tips": "名稱至少包含兩個字元,且不能和攝影機名或該攝影機下的其他區域同名。" + }, + "enabled": { + "title": "開啟", + "description": "指示該區域在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的區域在執行時會被忽略。" + }, + "inertia": { + "title": "慣性", + "desc": "辨識指定目標前該目標必須在這個區域內出現了多少幀。預設值:3" + }, + "loiteringTime": { + "title": "停留時間", + "desc": "設定目標必須在區域中至少要活動多少時間(單位為秒)。預設值:0" + }, + "objects": { + "title": "目標/物體", + "desc": "將在此區域應用的目標/物體類別清單。" + }, + "allObjects": "所有目標/物體", + "speedEstimation": { + "title": "速度估算", + "desc": "啟用此區域內物體的速度估算。該區域必須恰好包含 4 個點。", + "lineADistance": "A線距離({{unit}})", + "lineBDistance": "B線距離({{unit}})", + "lineCDistance": "C線距離({{unit}})", + "lineDDistance": "D線距離({{unit}})" + }, + "speedThreshold": { + "title": "速度閾值 ({{unit}})", + "desc": "指定物體在此區域內被視為有效的最低速度。", + "toast": { + "error": { + "pointLengthError": "此區域的速度估算已停用。啟用速度估算的區域必須恰好包含 4 個點。", + "loiteringTimeError": "徘徊時間大於 0 的區域不應與速度估算一起使用。" + } + } + }, + "toast": { + "success": "區域 ({{zoneName}}) 已儲存。" + } + }, + "motionMasks": { + "label": "畫面變動遮罩", + "documentTitle": "編輯畫面變動遮罩 - Frigate", + "desc": { + "title": "畫面變動遮罩用於防止觸發不必要的畫面變動偵測。過度的設定遮罩將使目標更加難以被追蹤。", + "documentation": "文件" + }, + "add": "新增畫面變動遮罩", + "edit": "編輯畫面變動遮罩", + "defaultName": "畫面變動遮罩 {{number}}", + "context": { + "title": "畫面變動遮罩用於防止不需要的畫面變動觸發偵測(例如:容易被風吹動的樹枝、攝影機畫面上顯示的時間等)。畫面變動遮罩應謹慎使用,過度的遮罩會導致追蹤目標變得更加困難。" + }, + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "名稱", + "description": "為該畫面變動遮罩設定別名(可選)。", + "placeholder": "輸入名稱…" + }, + "polygonAreaTooLarge": { + "title": "畫面變動遮罩的大小達到了攝影機畫面的{{polygonArea}}%。不建議設定太大的畫面變動遮罩。", + "tips": "畫面變動遮罩並不會使該區域無法偵測到指定目標/物體,如有需要,你應該使用 區域 來限制偵測的目標/物體型別。" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已儲存。", + "noName": "畫面變動遮罩已儲存。" + } + } + }, + "objectMasks": { + "label": "目標遮罩", + "documentTitle": "編輯目標遮罩 - Frigate", + "desc": { + "title": "目標過濾器用於防止特定位置出現對某個目標/物體的誤報。", + "documentation": "文件" + }, + "add": "新增目標遮罩", + "edit": "編輯目標遮罩", + "context": "目標過濾器用於防止特定位置的指定目標會誤報。", + "point_other": "{{count}} 點", + "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", + "name": { + "title": "名稱", + "description": "為該目標遮罩設定別名(可選)。", + "placeholder": "輸入名稱…" + }, + "objects": { + "title": "目標/物體", + "desc": "將應用於此目標遮罩的目標或物體型別。", + "allObjectTypes": "所有目標或物體型別" + }, + "toast": { + "success": { + "title": "{{polygonName}} 已儲存。", + "noName": "目標遮罩已儲存。" + } + } + }, + "masks": { + "enabled": { + "title": "開啟", + "description": "指示該遮罩在設定檔中是否處於啟用並啟用的狀態。若被停用,則無法透過 MQTT 啟用。停用的遮罩在執行時會被忽略。" + } + } + }, + "motionDetectionTuner": { + "title": "畫面變動偵測調整", + "unsavedChanges": "{{camera}} 的畫面變動調整設定未儲存", + "desc": { + "title": "Frigate 將首先使用畫面變動偵測來確認每一幀畫面中是否有變動的區域,然後再對該區域使用目標偵測。", + "documentation": "閱讀有關畫面變動偵測的文件" + }, + "Threshold": { + "title": "閾值", + "desc": "閾值決定像素亮度變化達到多少時會被認為是畫面變動。預設值:30" + }, + "contourArea": { + "title": "輪廓面積", + "desc": "輪廓面積值用於判斷產生了多大的變化區域可被認定為畫面變動。預設值:10" + }, + "improveContrast": { + "title": "提高對比度", + "desc": "提高較暗場景的對比度。預設值:啟用" + }, + "toast": { + "success": "畫面變動設定已儲存。" + } + }, + "debug": { + "title": "除錯", + "detectorDesc": "Frigate 將使用偵測器({{detectors}})來偵測攝影機影片流中的目標或物體。", + "desc": "除錯介面將即時顯示被追蹤的目標以及統計資訊,目標清單將顯示偵測到的目標和延遲顯示的概覽。", + "openCameraWebUI": "開啟 {{camera}} 的管理頁面", + "debugging": "除錯選項", + "objectList": "目標清單", + "noObjects": "沒有目標", + "audio": { + "title": "音訊", + "noAudioDetections": "未偵測到音訊事件", + "score": "分值", + "currentRMS": "當前均方根值(RMS)", + "currentdbFS": "當前滿量程相對分貝值(dbFS)" + }, + "boundingBoxes": { + "title": "邊界框", + "desc": "將在被追蹤的目標周圍顯示邊界框", + "colors": { + "label": "目標邊界框顏色定義", + "info": "
  • 啟用後,將會為每個目標的標籤分配不同的顏色
  • 深藍色細線代表該目標或物體在當前時間點未被偵測到
  • 灰色細線代表偵測到的目標或物體靜止不動
  • 粗線表示在啟動自動追蹤時,該目標為自動追蹤的主體
  • " + } + }, + "timestamp": { + "title": "時間戳", + "desc": "在影像上顯示時間戳" + }, + "zones": { + "title": "區域", + "desc": "顯示已定義的區域圖層" + }, + "mask": { + "title": "畫面變動遮罩", + "desc": "顯示畫面變動遮罩圖層" + }, + "motion": { + "title": "畫面變動區域框", + "desc": "在偵測到畫面變動的區域顯示區域框", + "tips": "

    畫面變動區域框


    將在當前偵測到畫面變動的區域內顯示紅色區域框。

    " + }, + "regions": { + "title": "範圍", + "desc": "顯示傳送給目標偵測器感興趣的區域框", + "tips": "

    範圍框


    將在幀中傳送到目標偵測器的感興趣範圍上疊加綠色框。

    " + }, + "paths": { + "title": "行動軌跡", + "desc": "顯示被追蹤目標的行動軌跡關鍵點", + "tips": "

    行動軌跡

    將使用線條來標示被追蹤目標在其活動週期內移動的關鍵位置點。

    " + }, + "objectShapeFilterDrawing": { + "title": "允許繪製“目標形狀過濾器”", + "desc": "在影像上繪製矩形,以檢視區域和比例詳細資訊", + "tips": "啟用此選項,能夠在攝影機畫面上繪製矩形,將顯示其區域和比例。你可以透過使用這些值在配置中設定目標形狀過濾器的引數。", + "score": "分數", + "ratio": "比例", + "area": "區域" + } + }, + "timestampPosition": { + "tl": "左上角", + "tr": "右上角", + "bl": "左下角", + "br": "右下角" + }, + "users": { + "title": "使用者", + "management": { + "title": "使用者管理", + "desc": "管理此 Frigate 例項的使用者帳戶。" + }, + "addUser": "新增使用者", + "updatePassword": "修改密碼", + "toast": { + "success": { + "createUser": "使用者 {{user}} 建立成功", + "deleteUser": "使用者 {{user}} 刪除成功", + "updatePassword": "已成功修改密碼。", + "roleUpdated": "已更新 {{user}} 的權限組" + }, + "error": { + "setPasswordFailed": "儲存密碼出現錯誤:{{errorMessage}}", + "createUserFailed": "建立使用者失敗:{{errorMessage}}", + "deleteUserFailed": "刪除使用者失敗:{{errorMessage}}", + "roleUpdateFailed": "更新權限組失敗:{{errorMessage}}" + } + }, + "table": { + "username": "使用者名稱", + "actions": "操作", + "role": "權限組", + "noUsers": "未找到使用者。", + "changeRole": "更改使用者權限組", + "password": "修改密碼", + "deleteUser": "刪除使用者" + }, + "dialog": { + "form": { + "user": { + "title": "使用者名稱", + "desc": "僅允許使用字母、數字、句點和下劃線。", + "placeholder": "請輸入使用者名稱" + }, + "password": { + "title": "密碼", + "placeholder": "請輸入密碼", + "show": "顯示密碼", + "hide": "隱藏密碼", + "confirm": { + "title": "確認密碼", + "placeholder": "請再次輸入密碼" + }, + "strength": { + "title": "密碼強度: ", + "weak": "弱", + "medium": "中等", + "strong": "強", + "veryStrong": "非常強" + }, + "requirements": { + "title": "密碼要求:", + "length": "至少需要 12 位字元" + }, + "match": "密碼匹配", + "notMatch": "密碼不匹配" + }, + "newPassword": { + "title": "新密碼", + "placeholder": "請輸入新密碼", + "confirm": { + "placeholder": "請再次輸入新密碼" + } + }, + "currentPassword": { + "title": "當前密碼", + "placeholder": "請輸入當前密碼" + }, + "usernameIsRequired": "使用者名稱為必填項", + "passwordIsRequired": "必須輸入密碼" + }, + "createUser": { + "title": "建立新使用者", + "desc": "建立一個新使用者帳戶,並指定一個權限組以控制存取 Frigate 頁面的權限。", + "usernameOnlyInclude": "使用者名稱只能包含字母、數字和 _", + "confirmPassword": "請確認你的密碼" + }, + "deleteUser": { + "title": "刪除該使用者", + "desc": "此操作無法撤銷。這將永久刪除使用者帳戶並移除所有相關資料。", + "warn": "你確定要刪除 {{username}} 嗎?" + }, + "passwordSetting": { + "cannotBeEmpty": "密碼不能為空", + "doNotMatch": "兩次輸入密碼不匹配", + "currentPasswordRequired": "當前密碼為必填", + "incorrectCurrentPassword": "當前密碼錯誤", + "passwordVerificationFailed": "驗證密碼失敗", + "updatePassword": "更新 {{username}} 的密碼", + "setPassword": "設定密碼", + "desc": "建立一個強密碼來保護此帳戶。", + "multiDeviceWarning": "其他已登入的裝置將需要在 {{refresh_time}} 內重新登入。", + "multiDeviceAdmin": "你也可以透過輪換你的 JWT 金鑰,強制所有使用者立即重新登入驗證。" + }, + "changeRole": { + "title": "更改使用者權限組", + "select": "選擇權限組", + "desc": "更新 {{username}} 的權限", + "roleInfo": { + "intro": "為該使用者選擇一個合適的權限組:", + "admin": "管理員", + "adminDesc": "完全功能與存取權限。", + "viewer": "成員", + "viewerDesc": "僅能夠檢視即時監控面板、審閱、瀏覽和匯出功能。", + "customDesc": "自訂特定攝影機的存取規則。" + } + } + } + }, + "roles": { + "management": { + "title": "成員權限組管理", + "desc": "管理此 Frigate 例項的自訂權限組及其攝影機存取權限。" + }, + "addRole": "新增權限組", + "table": { + "role": "權限組", + "cameras": "攝影機", + "actions": "操作", + "noRoles": "沒有找到自訂權限組。", + "editCameras": "編輯攝影機", + "deleteRole": "刪除權限組" + }, + "toast": { + "success": { + "createRole": "權限組 {{role}} 建立成功", + "updateCameras": "已更新攝影機至 {{role}} 權限組", + "deleteRole": "已刪除 {{role}} 權限組", + "userRolesUpdated_other": "已將分配到此權限組的 {{count}} 位使用者更新為 “成員”,該權限組可存取所有攝影機。" + }, + "error": { + "createRoleFailed": "建立權限組失敗:{{errorMessage}}", + "updateCamerasFailed": "更新攝影機失敗:{{errorMessage}}", + "deleteRoleFailed": "刪除權限組失敗:{{errorMessage}}", + "userUpdateFailed": "更新使用者權限組失敗:{{errorMessage}}" + } + }, + "dialog": { + "createRole": { + "title": "建立新權限組", + "desc": "新增新權限組並分配攝影機存取權限。" + }, + "editCameras": { + "title": "編輯權限組的攝影機", + "desc": "為權限組 {{role}} 更新攝影機存取權限。" + }, + "deleteRole": { + "title": "刪除權限組", + "desc": "此操作無法撤銷。這將永久刪除該權限組,並將所有擁有此權限組的使用者分配到 “成員” (view)權限組,該權限組將賦予使用者檢視所有攝影機的權限。", + "warn": "你確定要刪除權限組 {{role}} 嗎?", + "deleting": "刪除中…" + }, + "form": { + "role": { + "title": "權限組名稱", + "placeholder": "輸入權限組名稱", + "desc": "僅允許使用字母、數字、句點和下劃線。", + "roleIsRequired": "必須輸入權限組名稱", + "roleOnlyInclude": "權限組名稱僅支援字母、數字、英文句號和下劃線", + "roleExists": "該權限組名稱已存在。" + }, + "cameras": { + "title": "攝影機", + "desc": "請選擇該權限組能夠存取的攝影機。至少需要選擇一個攝影機。", + "required": "至少要選擇一個攝影機。" + } + } + } + }, + "notification": { + "title": "通知", + "notificationSettings": { + "title": "通知設定", + "desc": "Frigate 在瀏覽器中執行或作為 PWA 安裝時,可以原生向您的裝置傳送推送通知。" + }, + "notificationUnavailable": { + "title": "通知功能不可用", + "desc": "網頁推送通知需要安全連線(https://…)。這是瀏覽器的限制。請透過安全方式存取 Frigate 以使用通知功能。" + }, + "globalSettings": { + "title": "全域性設定", + "desc": "臨時暫停所有已註冊裝置上特定攝影機的通知。" + }, + "email": { + "title": "電子郵箱", + "placeholder": "例如:example@email.com", + "desc": "需要輸入有效的電子郵件,在推送服務出現問題時,將使用此電子郵件進行通知。" + }, + "cameras": { + "title": "攝影機", + "noCameras": "沒有可用的攝影機", + "desc": "選擇要啟用通知的攝影機。" + }, + "deviceSpecific": "裝置專用設定", + "registerDevice": "註冊該裝置", + "unregisterDevice": "取消註冊該裝置", + "sendTestNotification": "傳送測試通知", + "unsavedRegistrations": "未儲存通知註冊", + "unsavedChanges": "未儲存通知設定更改", + "active": "通知已啟用", + "suspended": "通知已暫停 {{time}}", + "suspendTime": { + "suspend": "暫停", + "5minutes": "暫停 5 分鐘", + "10minutes": "暫停 10 分鐘", + "30minutes": "暫停 30 分鐘", + "1hour": "暫停 1 小時", + "12hours": "暫停 12 小時", + "24hours": "暫停 24 小時", + "untilRestart": "暫停直到重啟" + }, + "cancelSuspension": "取消暫停", + "toast": { + "success": { + "registered": "已成功註冊通知。需要重啟 Frigate 才能傳送任何通知(包括測試通知)。", + "settingSaved": "通知設定已儲存。" + }, + "error": { + "registerFailed": "通知註冊失敗。" + } + } + }, + "frigatePlus": { + "title": "Frigate+ 設定", + "description": "Frigate+ 是一項訂閱服務,可為你的 Frigate 例項提供額外的功能和能力,包括使用基於你自己的資料訓練的自訂目標偵測模型。你可以在此管理 Frigate+ 的模型設定。", + "cardTitles": { + "api": "API", + "currentModel": "當前模型", + "otherModels": "其他模型", + "configuration": "配置" + }, + "apiKey": { + "title": "Frigate+ API 金鑰", + "validated": "Frigate+ API 金鑰已偵測並驗證透過", + "notValidated": "未偵測到 Frigate+ API 金鑰或驗證未透過", + "desc": "Frigate+ API 金鑰用於啟用與 Frigate+ 服務的整合。", + "plusLink": "瞭解更多關於 Frigate+" + }, + "snapshotConfig": { + "title": "快照配置", + "desc": "提交到 Frigate+ 需要同時在配置中開啟快照功能。", + "cleanCopyWarning": "部分攝影機未開啟快照功能", + "table": { + "camera": "攝影機", + "snapshots": "快照" + } + }, + "modelInfo": { + "title": "模型資訊", + "modelType": "模型型別", + "trainDate": "訓練日期", + "baseModel": "基礎模型", + "plusModelType": { + "baseModel": "基礎模型", + "userModel": "定向調優" + }, + "supportedDetectors": "支援的偵測器", + "cameras": "攝影機", + "loading": "正在載入模型資訊…", + "error": "載入模型資訊失敗", + "noModelLoaded": "目前未載入 Frigate+ 模型。", + "availableModels": "可用模型", + "loadingAvailableModels": "正在載入可用模型…", + "selectModel": "選擇模型", + "noModelsAvailable": "無可用模型", + "filter": { + "ariaLabel": "依類型篩選模型", + "baseModels": "基礎模型", + "fineTunedModels": "微調模型" + }, + "modelSelect": "您可以在Frigate+上選擇可用的模型。請注意,只能選擇與當前偵測器配置相容的模型。" + }, + "changeInDetectorsAndModel": "變更模型", + "unsavedChanges": "未儲存Frigate+變更設定", + "restart_required": "需要重啟(Frigate+模型已修改)", + "toast": { + "success": "Frigate+ 設定已儲存。請重啟 Frigate 以應用更改。", + "error": "配置更改儲存失敗:{{errorMessage}}" + } + }, + "detectorsAndModel": { + "title": "偵測器與模型", + "description": "設定執行物件偵測的偵測器後端及其使用的模型。變更會一起儲存以確保偵測器與模型保持同步。", + "cardTitles": { + "detector": "偵測器硬體", + "model": "偵測模型" + }, + "tabs": { + "plus": "Frigate+", + "custom": "自訂模型" + }, + "mismatch": { + "warning": "目前的 Frigate+ 模型「{{model}}」需要 {{required}} 偵測器。請在下方選擇相容的模型,或在儲存前切換到「自訂模型」。" + }, + "plusModel": { + "requiresDetector": "需要:{{detector}}", + "noModelSelected": "選擇 Frigate+ 模型" + }, + "toast": { + "saveSuccess": "偵測器與模型設定已儲存。請重新啟動 Frigate 以套用變更。", + "saveError": "儲存偵測器與模型設定失敗" + }, + "unsavedChanges": "偵測器與模型有未儲存的變更", + "restartRequired": "需要重新啟動(偵測器或模型已變更)" + }, + "maintenance": { + "title": "維護", + "sync": { + "title": "媒體同步", + "desc": "Frigate 會根據您的保留配置定期清理媒體檔案。出現少量孤立檔案是正常現象。使用此功能可以刪除磁碟上不再被資料庫引用的孤立媒體檔案。", + "started": "媒體同步已啟動。", + "alreadyRunning": "同步任務已在執行中", + "error": "啟動同步失敗", + "currentStatus": "狀態", + "jobId": "任務 ID", + "startTime": "開始時間", + "endTime": "結束時間", + "statusLabel": "狀態", + "results": "結果", + "errorLabel": "錯誤", + "mediaTypes": "媒體型別", + "allMedia": "所有媒體", + "dryRun": "試執行", + "dryRunEnabled": "不會刪除任何檔案", + "dryRunDisabled": "將刪除檔案", + "force": "強制執行", + "forceDesc": "繞過安全閾值,即使刪除超過 50% 的檔案也完成同步。", + "verbose": "詳細模式", + "verboseDesc": "將所有孤立檔案的完整清單寫入硬碟以供審閱。", + "running": "同步執行中…", + "start": "開始同步", + "inProgress": "同步正在進行中。此頁面已停用。", + "status": { + "queued": "已排隊", + "running": "執行中", + "completed": "已完成", + "failed": "失敗", + "notRunning": "未執行" + }, + "resultsFields": { + "filesChecked": "已檢查檔案", + "orphansFound": "發現孤立檔案", + "orphansDeleted": "已刪除孤立檔案", + "aborted": "已中止。刪除操作將超過安全閾值。", + "error": "錯誤", + "totals": "總計" + }, + "event_snapshots": "追蹤目標快照", + "event_thumbnails": "追蹤目標縮圖", + "review_thumbnails": "審閱縮圖", + "previews": "預覽", + "exports": "匯出", + "recordings": "錄影" + }, + "regionGrid": { + "title": "區域網格", + "desc": "區域網格是一種最佳化功能,它會學習不同大小的目標通常出現在每個攝影機視野中的位置。Frigate 利用這些資料來高效地確定偵測區域的大小。該網格會根據追蹤目標資料自動構建。", + "clear": "清除區域網格", + "clearConfirmTitle": "清除區域網格", + "clearConfirmDesc": "除非你最近更改了偵測器模型大小或攝影機的物理位置,並且遇到了目標追蹤問題,否則不建議清除區域網格。網格會隨著目標的追蹤自動重建。更改需要重啟 Frigate 才能生效。", + "clearSuccess": "區域網格清除成功", + "clearError": "清除區域網格失敗", + "restartRequired": "需要重啟以使區域網格更改生效" + } + }, + "configForm": { + "global": { + "title": "全域性設定", + "description": "這些設定適用於所有攝影機,除非在攝影機特定設定中被覆蓋。" + }, + "camera": { + "title": "攝影機設定", + "description": "這些設定僅適用於此攝影機,並會覆蓋全域性設定。", + "noCameras": "沒有可用的攝影機" + }, + "advancedSettingsCount": "高階設定 ({{count}})", + "advancedCount": "高階選項 ({{count}})", + "showAdvanced": "顯示高階設定", + "tabs": { + "sharedDefaults": "共享預設值", + "system": "系統", + "integrations": "整合" + }, + "additionalProperties": { + "keyLabel": "鍵", + "valueLabel": "值", + "keyPlaceholder": "新鍵名", + "remove": "移除" + }, + "knownPlates": { + "namePlaceholder": "例如:老婆的車", + "platePlaceholder": "車牌號或正則表示式" + }, + "timezone": { + "defaultOption": "使用瀏覽器時區" + }, + "roleMap": { + "empty": "未配置權限組對映", + "roleLabel": "角色", + "groupsLabel": "使用者組", + "addMapping": "新增角色對映", + "remove": "移除" + }, + "ffmpegArgs": { + "preset": "預設", + "manual": "手動引數", + "inherit": "繼承攝影機設定", + "none": "無", + "useGlobalSetting": "繼承全域性設定", + "selectPreset": "選擇預設", + "manualPlaceholder": "輸入 FFmpeg 引數", + "presetLabels": { + "preset-rpi-64-h264": "樹莓派(H.264)", + "preset-rpi-64-h265": "樹莓派(H.265)", + "preset-vaapi": "VAAPI (Intel/AMD GPU)", + "preset-intel-qsv-h264": "Intel QuickSync (H.264)", + "preset-intel-qsv-h265": "Intel QuickSync (H.265)", + "preset-nvidia": "NVIDIA GPU", + "preset-jetson-h264": "NVIDIA Jetson (H.264)", + "preset-jetson-h265": "NVIDIA Jetson (H.265)", + "preset-rkmpp": "瑞芯微 RKMPP", + "preset-http-jpeg-generic": "HTTP JPEG(通用)", + "preset-http-mjpeg-generic": "HTTP MJPEG(通用)", + "preset-http-reolink": "HTTP - Reolink 攝影機", + "preset-rtmp-generic": "RTMP(通用)", + "preset-rtsp-generic": "RTSP(通用)", + "preset-rtsp-restream": "RTSP - 從 go2rtc 轉流", + "preset-rtsp-restream-low-latency": "RTSP - 從 go2rtc 轉流(低延遲)", + "preset-rtsp-udp": "RTSP - UDP協議", + "preset-rtsp-blue-iris": "RTSP - Blue Iris", + "preset-record-generic": "錄製(通用,無音訊)", + "preset-record-generic-audio-copy": "錄製(通用,不轉碼音訊)", + "preset-record-generic-audio-aac": "錄製(通用並將音訊轉碼為 AAC)", + "preset-record-mjpeg": "錄製 - MJPEG 流攝影機", + "preset-record-jpeg": "錄製 - JPEG 流攝影機", + "preset-record-ubiquiti": "錄製 - 優必飛攝影機" + } + }, + "cameraInputs": { + "itemTitle": "影片流 {{index}}" + }, + "restartRequiredField": "需要重啟", + "restartRequiredFooter": "配置已更改 - 需要重啟", + "sections": { + "detect": "偵測", + "record": "錄製", + "snapshots": "快照", + "motion": "畫面變動", + "objects": "目標", + "review": "審閱", + "audio": "音訊", + "notifications": "通知", + "live": "即時檢視", + "timestamp_style": "時間戳", + "mqtt": "MQTT", + "database": "資料庫", + "telemetry": "遙測", + "auth": "身份驗證", + "tls": "TLS", + "proxy": "代理", + "go2rtc": "go2rtc", + "ffmpeg": "FFmpeg 編解碼", + "detectors": "偵測器", + "model": "模型", + "semantic_search": "語意搜尋", + "genai": "生成式 AI", + "face_recognition": "人臉辨識", + "lpr": "車牌辨識", + "birdseye": "鳥瞰圖", + "masksAndZones": "遮罩 / 區域" + }, + "detect": { + "title": "偵測設定" + }, + "detectors": { + "title": "偵測器設定", + "singleType": "只允許一個 {{type}} 偵測器。", + "keyRequired": "偵測器名稱為必填項。", + "keyDuplicate": "偵測器名稱已存在。", + "noSchema": "沒有可用的偵測器架構。", + "none": "未配置偵測器例項。", + "add": "新增偵測器", + "addCustomKey": "新增自訂鍵(Key)" + }, + "record": { + "title": "錄製設定" + }, + "snapshots": { + "title": "快照設定" + }, + "motion": { + "title": "畫面變動設定" + }, + "objects": { + "title": "目標設定" + }, + "audioLabels": { + "summary": "已選擇 {{count}} 個音訊標籤", + "empty": "無可用音訊標籤" + }, + "objectLabels": { + "summary": "已選擇 {{count}} 個目標型別", + "empty": "無可用目標標籤" + }, + "reviewLabels": { + "summary": "已選擇 {{count}} 個標籤", + "empty": "暫無可用標籤" + }, + "filters": { + "objectFieldLabel": "{{label}} 的 {{field}}" + }, + "zoneNames": { + "summary": "已選擇 {{count}} 個", + "empty": "沒有可用的區域" + }, + "inputRoles": { + "summary": "已選擇 {{count}} 個功能", + "empty": "無可用功能", + "options": { + "detect": "偵測", + "record": "錄製", + "audio": "音訊" + } + }, + "genaiRoles": { + "options": { + "embeddings": "嵌入(Embedding)", + "descriptions": "描述", + "chat": "對話" + } + }, + "semanticSearchModel": { + "placeholder": "選擇模型…", + "builtIn": "內建模型", + "genaiProviders": "生成式 AI 服務" + }, + "review": { + "title": "審閱設定" + }, + "audio": { + "title": "音訊設定" + }, + "notifications": { + "title": "通知設定" + }, + "live": { + "title": "即時檢視設定" + }, + "timestamp_style": { + "title": "時間戳設定" + }, + "searchPlaceholder": "搜尋…", + "addCustomLabel": "新增自訂標籤…", + "genaiModel": { + "placeholder": "選擇模型…", + "search": "搜尋模型…", + "noModels": "暫無模型" + } + }, + "globalConfig": { + "title": "全域性配置", + "description": "配置適用於所有攝影機的全域性設定,除非被單獨覆蓋。", + "toast": { + "success": "全域性設定儲存成功", + "error": "儲存全域性設定失敗", + "validationError": "驗證失敗" + } + }, + "cameraConfig": { + "title": "攝影機配置", + "description": "配置單個攝影機的設定。這些設定會覆蓋全域性預設值。", + "overriddenBadge": "已覆蓋", + "resetToGlobal": "重設為全域性設定", + "toast": { + "success": "攝影機設定儲存成功", + "error": "儲存攝影機設定失敗" + } + }, + "toast": { + "success": "設定儲存成功", + "applied": "設定應用成功", + "successRestartRequired": "設定儲存成功。請重啟 Frigate 以應用更改。", + "error": "儲存設定失敗", + "validationError": "驗證失敗:{{message}}", + "resetSuccess": "已重設為全域性預設值", + "resetError": "重設設定失敗", + "saveAllSuccess_other": "所有 {{count}} 個部分儲存成功。", + "saveAllPartial_other": "已儲存 {{successCount}} / {{totalCount}} 個部分。{{failCount}} 個失敗。", + "saveAllFailure": "儲存所有部分失敗。" + }, + "profiles": { + "title": "設定檔", + "activeProfile": "啟用設定檔", + "noActiveProfile": "無啟用的設定檔", + "active": "啟用", + "activated": "設定檔 {{profile}} 已啟用", + "activateFailed": "設定檔設定失敗", + "deactivated": "設定檔已停用", + "noProfiles": "未定義任何設定檔。", + "noOverrides": "無覆蓋項", + "cameraCount_other": "{{count}} 個攝影機", + "columnCamera": "攝影機", + "columnOverrides": "設定檔覆蓋", + "baseConfig": "基礎配置", + "addProfile": "新增設定檔", + "newProfile": "新設定檔", + "profileNamePlaceholder": "例如:佈防、外出、夜間模式", + "friendlyNameLabel": "設定檔名稱", + "profileIdLabel": "設定檔 ID", + "profileIdDescription": "用於配置和自動化的內部辨識符號", + "nameInvalid": "僅允許使用小寫字母、數字和下劃線", + "nameDuplicate": "已存在同名設定檔", + "error": { + "mustBeAtLeastTwoCharacters": "至少需要 2 個字元", + "mustNotContainPeriod": "不得包含英文句號(\".\")", + "alreadyExists": "已存在使用此 ID 的設定檔" + }, + "renameProfile": "重新命名設定檔", + "renameSuccess": "已將設定檔重新命名為 “{{profile}}”", + "deleteProfile": "刪除設定檔", + "deleteProfileConfirm": "確定要為所有攝影機刪除設定檔“{{profile}}”嗎?該步驟無法撤銷。", + "deleteSuccess": "設定檔“{{profile}}”已刪除", + "createSuccess": "設定檔“{{profile}}”已建立", + "removeOverride": "移除設定檔覆蓋", + "deleteSection": "刪除節點覆蓋", + "deleteSectionConfirm": "是否要移除攝像機 {{camera}} 上針對設定檔 {{profile}} 的 {{section}} 覆蓋設定?", + "deleteSectionSuccess": "已移除 {{profile}} 的 {{section}} 覆蓋設定", + "enableSwitch": "開啟設定檔", + "enabledDescription": "設定檔功能已啟用。請在下方建立新的設定檔,進入攝影機配置頁面進行修改並儲存,修改即可生效。", + "disabledDescription": "設定檔功能可以讓你建立一組帶名稱的攝影機自訂引數(比如佈防、離家、夜間模式),並隨時切換啟用。" + }, + "unsavedChanges": "您有未儲存的更改", + "confirmReset": "確認重設", + "resetToDefaultDescription": "這將把此部分的所有設定重設為預設值。此操作無法撤銷。", + "resetToGlobalDescription": "這將把此部分的設定重設為全域性預設值。此操作無法撤銷。", + "go2rtcStreams": { + "title": "go2rtc 影片流", + "description": "管理用於攝影機轉流的 go2rtc 流配置。每個影片流包含一個名稱以及一個或多個源地址 URL。", + "addStream": "新增影片流", + "addStreamDesc": "為新的影片流輸入一個名稱,該名稱將用於在攝影機配置中引用該影片流。", + "addUrl": "新增 URL 地址", + "streamName": "影片流名稱", + "streamNamePlaceholder": "例如:front_door,此處只能使用英文", + "streamUrlPlaceholder": "例如:rtsp://user:pass@192.168.1.100/stream", + "deleteStream": "刪除影片流", + "deleteStreamConfirm": "確定要刪除影片流 “{{streamName}}” 嗎?引用該影片流的攝影機可能會停止工作。", + "noStreams": "未配置任何 go2rtc 流。請新增一個影片流以開始使用。", + "validation": { + "nameRequired": "影片流名稱為必填", + "nameDuplicate": "已存在同名的影片流", + "nameInvalid": "影片流名稱只能使用字母、數字、下劃線和連字元", + "urlRequired": "至少需要填寫一個 URL 地址" + }, + "renameStream": "重新命名影片流", + "renameStreamDesc": "為此影片流輸入新名稱。重新命名影片流可能會導致透過名稱引用它的攝影機或其他流無法正常工作。", + "newStreamName": "新影片流名稱", + "ffmpeg": { + "useFfmpegModule": "使用相容模式(ffmpeg)", + "video": "影片", + "audio": "音訊", + "hardware": "硬體加速", + "videoCopy": "直接複製", + "videoH264": "轉碼為 H.264", + "videoH265": "轉碼為 H.265", + "videoExclude": "排除", + "audioCopy": "直接複製", + "audioAac": "轉碼為 AAC", + "audioOpus": "轉碼為 Opus", + "audioPcmu": "轉碼為 PCM μ-law", + "audioPcma": "轉碼為 PCM A-law", + "audioPcm": "轉碼為 PCM", + "audioMp3": "轉碼為 MP3", + "audioExclude": "排除", + "hardwareNone": "無硬體加速", + "hardwareAuto": "自動選擇硬體加速" + } + }, + "birdseye": { + "trackingMode": { + "objects": "目標", + "motion": "動作", + "continuous": "持續" + } + }, + "retainMode": { + "all": "全部", + "motion": "動作", + "active_objects": "活動目標" + }, + "previewQuality": { + "very_high": "極高", + "high": "高", + "medium": "中", + "low": "低", + "very_low": "極低" + }, + "ui": { + "timeFormat": { + "browser": "瀏覽器", + "12hour": "12 小時", + "24hour": "24 小時" + }, + "TimeOrDateStyle": { + "full": "完整", + "long": "長", + "medium": "中", + "short": "短" + }, + "unitSystem": { + "metric": "公制", + "imperial": "英制" + } + }, + "review": { + "imageSource": { + "recordings": "錄影", + "previews": "預覽" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Warning", + "error": "Error", + "critical": "Critical" + } + }, + "onvif": { + "profileAuto": "自動", + "profileLoading": "正在載入設定檔…", + "autotracking": { + "zooming": { + "disabled": "停用", + "absolute": "絕對", + "relative": "相對" + } + } + }, + "modelSize": { + "small": "小", + "large": "大" + }, + "configMessages": { + "review": { + "recordDisabled": "錄製已停用,不會生成審閱記錄項。", + "detectDisabled": "目標偵測已停用。審閱記錄需要依靠偵測到的目標來對警報和偵測事件進行分類。", + "allNonAlertDetections": "所有非警報類活動都將被記錄為偵測事件。", + "genaiImageSourceRecordingsRecordDisabled": "影像源雖然設定為“錄製”,但錄製功能已關閉。Frigate 將自動降級使用預覽圖片。" + }, + "audio": { + "noAudioRole": "暫無任何流已開啟音訊(audio)功能(role)。必須在影片流上啟用音訊功能,音訊偵測才能正常工作。" + }, + "audioTranscription": { + "audioDetectionDisabled": "該攝影機未開啟音訊偵測功能。音訊轉錄需要先開啟音訊偵測。" + }, + "detect": { + "fpsGreaterThanFive": "不建議設定偵測幀率高於 5,數值設定過高可能引發效能問題,且不會帶來任何增益。", + "disabled": "目標偵測已停用。快照、回放條目以及人臉辨識、車牌辨識、生成式 AI 等增強功能都將無法使用。" + }, + "objects": { + "genaiNoDescriptionsProvider": "必須配置具備“描述”功能的生成式 AI 服務商,才能自動生成事件描述。" + }, + "faceRecognition": { + "globalDisabled": "必須開啟人臉辨識增強功能,此攝影機的人臉辨識相關功能才能正常使用。", + "personNotTracked": "人臉辨識需要偵測到 “人”(person) 後才能工作。請在該攝影機的偵測目標設定中新增“人”。", + "modelSizeLarge": "大型模型需要 GPU 或 NPU 才能執行正常。僅使用 CPU 的裝置請選用小型模型。" + }, + "lpr": { + "globalDisabled": "要讓該攝影機的車牌辨識功能正常使用,必須先開啟車牌辨識增強功能。", + "vehicleNotTracked": "車牌辨識需要先開啟對 “汽車” 或 “摩托車” 的目標追蹤。請在該攝影機的偵測目標中新增“汽車”或“摩托車”。", + "modelSizeLarge": "大型模型針對多行格式車牌做了最佳化。小型模型的效能優於大型模型,而且只有小型模型才能支援中文車牌。除非你所在地區使用多行車牌格式,否則建議使用小型模型。" + }, + "record": { + "noRecordRole": "暫無任何影片流已配置錄製功能,錄製功能將無法正常工作。" + }, + "birdseye": { + "objectsModeDetectDisabled": "鳥瞰圖已設定為 “目標” 模式,但此攝影機未開啟目標偵測。該攝影機將不會顯示在鳥瞰畫面中。" + }, + "snapshots": { + "detectDisabled": "目標偵測已停用。快照是根據追蹤到的目標生成的,因此將不會建立快照。" + }, + "detectors": { + "mixedTypes": "所有偵測器必須為同一型別。若要更換為其他型別,請先移除現有的偵測器。", + "mixedTypesSuggestion": "所有偵測器必須使用相同型別。請移除現有偵測器,或選擇 {{type}}。" + }, + "semanticSearch": { + "jinav2SmallModelSize": "Jina V2 的大型模型版本記憶體佔用與推理開銷較高,建議搭配獨立顯示卡使用大型模型。" } } } diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json index e956b9a42e..23aa19f880 100644 --- a/web/public/locales/zh-Hant/views/system.json +++ b/web/public/locales/zh-Hant/views/system.json @@ -7,7 +7,8 @@ "logs": { "frigate": "Frigate 日誌 - Frigate", "go2rtc": "Go2RTC 日誌 - Frigate", - "nginx": "Nginx 日誌 - Frigate" + "nginx": "Nginx 日誌 - Frigate", + "websocket": "訊息日誌 - Frigate" } }, "title": "系統", @@ -33,6 +34,33 @@ "fetchingLogsFailed": "擷取日誌時出錯:{{errorMessage}}", "whileStreamingLogs": "串流日誌時出錯:{{errorMessage}}" } + }, + "websocket": { + "label": "訊息", + "pause": "暫停", + "resume": "繼續", + "clear": "清除", + "filter": { + "all": "全部主題", + "topics": "主題", + "events": "事件", + "reviews": "審閱", + "classification": "分類", + "face_recognition": "人臉辨識", + "lpr": "車牌辨識", + "camera_activity": "攝影機活動", + "system": "系統", + "camera": "攝影機", + "all_cameras": "所有攝影機", + "cameras_count_one": "{{count}} 個攝影機", + "cameras_count_other": "{{count}} 個攝影機" + }, + "empty": "未捕獲到訊息", + "count_one": "{{count}} 則訊息", + "count_other": "{{count}} 則訊息", + "expanded": { + "payload": "Payload" + } } }, "general": { @@ -81,7 +109,10 @@ "title": "Intel GPU 狀態警告", "message": "GPU 狀態資訊不可用", "description": "這是一個在Intel GPU 狀態回報工具 (intel_gpu_top) 中已知的 Bug,該工具會故障並重複的回報 GPU占用率為 0%,甚至在硬體加速與物件偵測在 (i)GPU上正確運作時也是如此。這不是 Frigate 的 Bug。您可以透過重新啟動主機來暫時修復此問題以確認 GPU 運作正常。這不會影響效能。" - } + }, + "gpuCompute": "GPU 計算 / 編碼", + "gpuTemperature": "GPU 溫度", + "npuTemperature": "NPU 溫度" }, "otherProcesses": { "title": "其他行程", @@ -118,7 +149,11 @@ }, "shm": { "title": "SHM(共享記憶體)配置", - "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。" + "warning": "目前的 SHM 大小為 {{total}}MB,過小。請將其增加至至少 {{min_shm}}MB。", + "frameLifetime": { + "title": "幀保留時間", + "description": "每個攝影機在共享記憶體中擁有 {{frames}} 個幀槽位。在最快攝影機的幀率下,每一幀在被覆蓋前大約可保留 {{lifetime}} 秒。" + } } }, "cameras": { @@ -156,7 +191,8 @@ "cameraDetect": "{{camName}} 偵測", "cameraFramesPerSecond": "{{camName}} 幀率", "cameraDetectionsPerSecond": "{{camName}} 每秒偵測幀率", - "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率" + "cameraSkippedDetectionsPerSecond": "{{camName}} 每秒跳過偵測幀率", + "cameraGpu": "{{camName}} GPU" }, "toast": { "success": { @@ -165,6 +201,20 @@ "error": { "unableToProbeCamera": "無法檢測鏡頭:{{errorMessage}}" } + }, + "noCameras": { + "title": "沒有找到攝影機" + }, + "connectionQuality": { + "title": "連線品質", + "excellent": "優秀", + "fair": "一般", + "poor": "較差", + "unusable": "不可用", + "fps": "幀率", + "expectedFps": "預期幀率", + "reconnectsLastHour": "最近一小時重連次數", + "stallsLastHour": "最近一小時卡頓次數" } }, "lastRefreshed": "最後更新: ", @@ -176,7 +226,8 @@ "cameraIsOffline": "{{camera}} 已離線", "detectIsSlow": "{{detect}} 偵測速度較慢({{speed}} 毫秒)", "detectIsVerySlow": "{{detect}} 偵測速度緩慢({{speed}} 毫秒)", - "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。" + "shmTooLow": "/dev/shm 配置({{total}} MB)應增加至至少{{min}} MB。", + "debugReplayActive": "除錯回放工作階段正在進行" }, "enrichments": { "title": "進階功能", From 6ffb9f2c9ecc6807c55247df166f5b1cf4d917e2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:17:03 +0200 Subject: [PATCH 26/94] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1162 of 1162 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1150 of 1150 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.8% (1128 of 1141 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1122 of 1122 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (792 of 792 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (471 of 471 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (127 of 127 strings) Co-authored-by: Edward Zhang Co-authored-by: GuoQing Liu <842607283@qq.com> Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-settings --- web/public/locales/zh-CN/common.json | 3 +- web/public/locales/zh-CN/config/cameras.json | 16 +- web/public/locales/zh-CN/config/global.json | 30 ++-- web/public/locales/zh-CN/objects.json | 7 +- web/public/locales/zh-CN/views/chat.json | 18 ++ .../locales/zh-CN/views/faceLibrary.json | 6 +- web/public/locales/zh-CN/views/settings.json | 154 ++++++++++++++++-- 7 files changed, 199 insertions(+), 35 deletions(-) diff --git a/web/public/locales/zh-CN/common.json b/web/public/locales/zh-CN/common.json index 5771a2288e..1fa683f441 100644 --- a/web/public/locales/zh-CN/common.json +++ b/web/public/locales/zh-CN/common.json @@ -221,7 +221,8 @@ "gl": "加利西亚语 (Galego)", "id": "印度尼西亚语 (Bahasa Indonesia)", "ur": "乌尔都语 (اردو)", - "hr": "克罗地亚语 (Hrvatski)" + "hr": "克罗地亚语 (Hrvatski)", + "bs": "波斯尼亚语(Bosanski)" }, "appearance": "外观", "darkMode": { diff --git a/web/public/locales/zh-CN/config/cameras.json b/web/public/locales/zh-CN/config/cameras.json index 5bd976c693..73e4fecde7 100644 --- a/web/public/locales/zh-CN/config/cameras.json +++ b/web/public/locales/zh-CN/config/cameras.json @@ -37,7 +37,11 @@ }, "filters": { "label": "音频过滤器", - "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。" + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } }, "enabled_in_config": { "label": "原始音频状态", @@ -68,7 +72,7 @@ }, "mode": { "label": "追踪模式", - "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。" + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" }, "order": { "label": "排序位置", @@ -603,7 +607,7 @@ }, "image_source": { "label": "核查图像来源", - "description": "发送给生成式 AI 的画面来源('preview' 或 'recordings');'recordings' 使用更高质量的画面帧,但会消耗更多的 token。" + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" }, "additional_concerns": { "label": "额外关注事项", @@ -723,7 +727,7 @@ "description": "摄像头特定语义搜索触发器的操作和匹配条件。", "friendly_name": { "label": "友好名称", - "description": "在 UI 中为此触发器显示的可选友好名称。" + "description": "可选友好名称,用于在界面上为触发器显示此名称。" }, "enabled": { "label": "开启此触发器", @@ -852,7 +856,7 @@ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" }, "dashboard": { - "label": "在 UI 中显示", + "label": "在页面中显示", "description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。" } }, @@ -873,7 +877,7 @@ "description": "区域允许您定义帧的特定区域,以便确定目标是否在特定区域内。", "friendly_name": { "label": "区域名称", - "description": "区域的友好名称,显示在 Frigate UI 中。如果未设置,将使用区域名称的格式化版本。" + "description": "区域的友好名称,显示在 Frigate 页面中。如果未设置,将使用区域名称的格式化版本。" }, "enabled": { "label": "开启", diff --git a/web/public/locales/zh-CN/config/global.json b/web/public/locales/zh-CN/config/global.json index fed5425d7b..ddfeb01be1 100644 --- a/web/public/locales/zh-CN/config/global.json +++ b/web/public/locales/zh-CN/config/global.json @@ -48,7 +48,11 @@ }, "filters": { "label": "音频过滤器", - "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。" + "description": "按音频类型的过滤器设置,如用于减少误报的置信度阈值。", + "threshold": { + "label": "最低音频置信度", + "description": "设置音频事件所需的最低置信度阈值。" + } }, "enabled_in_config": { "label": "原始音频状态", @@ -136,7 +140,7 @@ }, "mode": { "label": "追踪模式", - "description": "在鸟瞰视图中包含摄像头的模式:'objects'(目标)、'motion'(动作)或 'continuous'(持续)。" + "description": "在鸟瞰视图中包含摄像头的模式有:“基于目标”、“基于画面变动”或“连续”。" }, "order": { "label": "排序位置", @@ -252,7 +256,7 @@ "description": "所有摄像头的人脸检测和识别设置;可按摄像头覆盖。", "model_size": { "label": "模型大小", - "description": "用于人脸嵌入的模型大小(small/large);较大的可能需要 GPU。" + "description": "用于人脸嵌入的模型大小(小型/大型);较大的可能需要 GPU。" }, "unknown_score": { "label": "未知分数阈值", @@ -544,19 +548,19 @@ "description": "用户界面偏好设置,如时区、时间/日期格式和单位。", "timezone": { "label": "时区", - "description": "UI 中显示的可选时区(如果未设置,则默认为浏览器本地时间)。" + "description": "可选时区,用于整个界面展示时间(如果未设置,则默认为浏览器本地时间的时区)。" }, "time_format": { "label": "时间格式", - "description": "UI 中使用的时间格式(browser、12hour 或 24hour)。" + "description": "页面中将使用的时间格式(浏览器、12小时制 或 24小时制)。" }, "date_style": { "label": "日期样式", - "description": "UI 中使用的日期样式(full、long、medium、short)。" + "description": "页面中将使用的日期样式(完整、长、中等、短)。" }, "time_style": { "label": "时间样式", - "description": "UI 中使用的时间样式(full、long、medium、short)。" + "description": "页面中将使用的时间样式(完整、长、中等、短)。" }, "unit_system": { "label": "单位系统", @@ -1756,7 +1760,7 @@ }, "review": { "label": "核查", - "description": "控制 UI 和存储使用的警报、检测和 GenAI 核查摘要的设置。", + "description": "控制界面与存储所使用的警报、检测和生成式 AI 核查总结的相关设置。", "alerts": { "label": "警报配置", "description": "哪些追踪目标生成警报以及如何保留警报的设置。", @@ -1822,7 +1826,7 @@ }, "image_source": { "label": "核查图像来源", - "description": "发送给生成式 AI 的画面来源('preview' 或 'recordings');'recordings' 使用更高质量的画面帧,但会消耗更多的 token。" + "description": "发送给生成式 AI 的画面来源(“预览” 或 “录制”);“录制”将使用更高质量的画面帧,但会消耗更多的 token。" }, "additional_concerns": { "label": "额外关注事项", @@ -2015,7 +2019,7 @@ }, "model_size": { "label": "模型大小", - "description": "选择模型大小;'small' 在 CPU 上运行,'large' 通常需要 GPU。" + "description": "选择模型大小;“小型”模型一般在 CPU 上运行,而“大型”模型通常需要 GPU。" }, "device": { "label": "设备", @@ -2026,7 +2030,7 @@ "description": "摄像头特定语义搜索触发器的操作和匹配条件。", "friendly_name": { "label": "友好名称", - "description": "在 UI 中为此触发器显示的可选友好名称。" + "description": "可选友好名称,用于在界面上为触发器显示此名称。" }, "enabled": { "label": "开启此触发器", @@ -2059,7 +2063,7 @@ }, "model_size": { "label": "模型大小", - "description": "用于文本检测/识别的模型大小,大多数用户应使用 'small',只有'small'模型支持中文。" + "description": "用于文本检测/识别的模型大小,大多数用户应使用“小型”模型,而且只有“小型”模型支持中文车牌。" }, "detection_threshold": { "label": "检测阈值", @@ -2172,7 +2176,7 @@ "description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。" }, "dashboard": { - "label": "在 UI 中显示", + "label": "在页面中显示", "description": "切换此摄像头在 Frigate 页面中是否可见。禁用后需要手动编辑配置才能再次在页面中查看此摄像头。" } }, diff --git a/web/public/locales/zh-CN/objects.json b/web/public/locales/zh-CN/objects.json index f8d07bc23b..59058f332d 100644 --- a/web/public/locales/zh-CN/objects.json +++ b/web/public/locales/zh-CN/objects.json @@ -121,5 +121,10 @@ "royal_mail": "英国皇家邮政", "school_bus": "校车", "skunk": "臭鼬", - "kangaroo": "袋鼠" + "kangaroo": "袋鼠", + "baby": "婴儿", + "baby_stroller": "婴儿车", + "rickshaw": "三轮车", + "Rodent": "啮齿动物", + "rodent": "鼠类动物" } diff --git a/web/public/locales/zh-CN/views/chat.json b/web/public/locales/zh-CN/views/chat.json index 894b3c6d50..429dd56677 100644 --- a/web/public/locales/zh-CN/views/chat.json +++ b/web/public/locales/zh-CN/views/chat.json @@ -42,5 +42,23 @@ "show_camera_status": "我的摄像头当前状态如何?", "recap": "我不在的时候发生了什么事?", "watch_camera": "监控前门,有人出现就通知我" + }, + "new_chat": "新对话", + "settings": { + "title": "对话设置", + "show_stats": { + "title": "显示统计数据", + "desc": "显示对话回复的生成速率和上下文大小。", + "while_generating": "正在生成中", + "always": "始终" + }, + "auto_scroll": { + "title": "自动滚动", + "desc": "自动滚动到最新消息。" + } + }, + "stats": { + "context": "{{tokens}} 词元(tokens)", + "tokens_per_second": "{{rate}} 词元/秒" } } diff --git a/web/public/locales/zh-CN/views/faceLibrary.json b/web/public/locales/zh-CN/views/faceLibrary.json index d383fb348a..59aedc9f1d 100644 --- a/web/public/locales/zh-CN/views/faceLibrary.json +++ b/web/public/locales/zh-CN/views/faceLibrary.json @@ -30,7 +30,11 @@ "title": "近期识别记录", "aria": "选择近期识别记录", "empty": "近期未检测到人脸识别操作", - "titleShort": "近期" + "titleShort": "近期", + "emptyNoLibrary": { + "title": "更新人脸", + "description": "你必须向库中添加至少一张人脸,人脸识别功能才能正常工作。" + } }, "selectItem": "选择 {{item}}", "selectFace": "选择人脸", diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index 3831dfc56c..0a181dee18 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -16,7 +16,8 @@ "globalConfig": "全局配置 - Frigate", "cameraConfig": "摄像头配置 - Frigate", "maintenance": "维护 - Frigate", - "profiles": "配置模板 - Frigate" + "profiles": "配置模板 - Frigate", + "detectorsAndModel": "检测器和模型 - Frigate" }, "menu": { "ui": "界面设置", @@ -52,7 +53,7 @@ "systemTls": "TLS加密链接", "systemAuthentication": "验证", "systemNetworking": "网络", - "systemProxy": "代理", + "systemProxy": "反向代理", "systemUi": "界面", "systemLogging": "日志", "systemEnvironmentVariables": "环境变量", @@ -92,7 +93,8 @@ "uiSettings": "界面设置", "profiles": "配置模板", "systemGo2rtcStreams": "go2rtc 视频流", - "maintenance": "维护" + "maintenance": "维护", + "systemDetectorsAndModel": "检测器和模型" }, "dialog": { "unsavedChanges": { @@ -795,12 +797,20 @@ "cameras": "摄像头", "loading": "正在加载模型信息…", "error": "加载模型信息失败", - "availableModels": "可用模型", + "availableModels": "可用 Frigate+ 模型", "loadingAvailableModels": "正在加载可用模型…", "modelSelect": "您可以在Frigate+上选择可用的模型。请注意,只能选择与当前检测器配置兼容的模型。", "plusModelType": { "baseModel": "基础模型", "userModel": "定向调优" + }, + "noModelLoaded": "当前未加载任何Frigate+模型。", + "selectModel": "选择一个模型", + "noModelsAvailable": "无可用模型", + "filter": { + "ariaLabel": "按类型筛选模型", + "baseModels": "基础模型", + "fineTunedModels": "微调过的模型" } }, "toast": { @@ -815,7 +825,8 @@ "currentModel": "当前模型", "otherModels": "其他模型", "configuration": "配置" - } + }, + "changeInDetectorsAndModel": "改变模型" }, "enrichments": { "title": "增强功能设置", @@ -1353,7 +1364,7 @@ "title": "开启或关闭摄像头", "desc": "将临时禁用摄像头,直到 Frigate 重启。禁用摄像头将完全停止 Frigate 对该摄像头视频流的处理,届时检测、录制及调试功能均不可用。
    注意:go2rtc 的转流服务不受影响。", "enableLabel": "开启摄像头", - "enableDesc": "暂时禁用已开启的摄像头,直到 Frigate 重启。禁用摄像头会完全停止 Frigate 对该摄像头视频流的处理。检测、录像和调试功能将不可用。
    注意:这不会禁用 go2rtc 的转推流。", + "enableDesc": "暂时禁用已开启的摄像头,直到 Frigate 重启。禁用摄像头会完全停止 Frigate 对该摄像头视频流的处理。检测、录像和调试功能将不可用。
    注意:这不会禁用 go2rtc 的转推流。

    拖动滑块以重新排序摄像头,使其在用户界面中按顺序显示。启用的摄像头的顺序将在整个用户界面中反映,包括实时监控仪表板和摄像头选择下拉菜单。", "disableLabel": "关闭摄像头", "disableDesc": "开启在当前在界面中不可见且在配置中被禁用的摄像头。启用后需要重启 Frigate 才能生效。", "enableSuccess": "已在配置中启用 {{cameraName}}。请重启 Frigate 以应用更改。", @@ -1362,7 +1373,10 @@ "title": "修改显示名称", "description": "设置该摄像机在 Frigate 用户界面中显示的名称。若留空,则使用摄像机 ID。", "rename": "重命名" - } + }, + "reorderHandle": "拖动以重新排序", + "saving": "保存中…", + "saved": "已保存" }, "cameraConfig": { "add": "添加摄像头", @@ -1416,11 +1430,12 @@ "cameraType": { "title": "摄像头类型", "label": "摄像头类型", - "description": "为每路摄像头设置类型。专用车牌识别(LPR)摄像头为单用途设备,配备高倍光学变焦,可抓拍远处车辆的车牌。绝大多数摄像头应选用通用类型;只有专为车牌识别部署、且画面聚焦对准车牌的摄像头,才需选择专用 LPR 类型。", + "description": "为每路摄像头设置类型。专用车牌识别(LPR)摄像头为单用途设备,配备高倍光学变焦,可抓拍远处车辆的车牌。绝大多数摄像头应选用“通用”类型;只有专为车牌识别部署、且画面聚焦对准车牌的摄像头,才需选择“专用车牌识别”。", "normal": "通用", "dedicatedLpr": "车牌识别专用", "saveSuccess": "已更新 {{cameraName}} 的摄像头类型,请重启 Frigate 以使更改生效。" - } + }, + "description": "添加、编辑和删除摄像头,控制启用哪些摄像头,并配置每个配置文件和摄像头类型的覆盖设置。要配置流媒体、检测、运动和其他特定于摄像头的设置,请在“摄像头配置”下选择相关功能。" }, "cameraReview": { "title": "摄像头核查设置", @@ -1659,7 +1674,9 @@ "options": { "embeddings": "嵌入(Embedding)", "vision": "视觉(Vision)", - "tools": "工具(Tools)" + "tools": "工具(Tools)", + "descriptions": "描述生成", + "chat": "聊天对话" } }, "semanticSearchModel": { @@ -1771,7 +1788,8 @@ "resetError": "重置设置失败", "saveAllSuccess_other": "所有 {{count}} 个部分保存成功。", "saveAllPartial_other": "已保存 {{successCount}} / {{totalCount}} 个部分。{{failCount}} 个失败。", - "saveAllFailure": "保存所有部分失败。" + "saveAllFailure": "保存所有部分失败。", + "saveAllSuccessRestartRequired_other": "成功保存 {{count}} 个部分。重启 Frigate 以生效。" }, "unsavedChanges": "您有未保存的更改", "confirmReset": "确认重置", @@ -1788,7 +1806,11 @@ "heading_other": "此全局设置项下有 {{count}} 个摄像头存在自定义单独配置。", "othersField_other": "其余 {{count}} 个", "profilePrefix": "{{profile}} 配置方案:{{fields}}" - } + }, + "overriddenGlobalHeading_other": "该摄像头已覆盖全局配置中的 {{count}} 项设置:", + "overriddenGlobalNoDeltas": "该摄像头已覆盖全局配置,但所有配置项数值均无差异。", + "overriddenBaseConfigHeading_other": "{{profile}} 配置模板已覆盖基础配置中的 {{count}} 项设置:", + "overriddenBaseConfigNoDeltas": "{{profile}} 配置模板已覆盖该板块,但各项参数与基础配置完全一致无差异。" }, "profiles": { "title": "配置模板", @@ -1881,7 +1903,14 @@ }, "onvif": { "profileAuto": "自动", - "profileLoading": "正在加载配置文件…" + "profileLoading": "正在加载配置文件…", + "autotracking": { + "zooming": { + "disabled": "关闭", + "absolute": "绝对", + "relative": "相对" + } + } }, "configMessages": { "review": { @@ -1929,5 +1958,104 @@ "semanticSearch": { "jinav2SmallModelSize": "Jina V2 的大型模型版本内存占用与推理开销较高,建议搭配独立显卡使用大型模型。" } + }, + "birdseye": { + "trackingMode": { + "objects": "基于目标", + "motion": "基于画面变动", + "continuous": "连续" + }, + "cameraOrder": { + "label": "摄像头排序", + "description": "拖动摄像头以在鸟瞰布局中设置它们的顺序。", + "reorderHandle": "拖动以重新排序", + "saving": "保存中…", + "saved": "已保存" + } + }, + "snapshot": { + "retainMode": { + "all": "所有", + "motion": "画面变动", + "active_objects": "活动目标" + } + }, + "ui": { + "timeFormat": { + "browser": "基于浏览器", + "12hour": "12 小时制", + "24hour": "24 小时制" + }, + "TimeOrDateStyle": { + "full": "完整", + "long": "长", + "medium": "中等", + "short": "段" + }, + "unitSystem": { + "metric": "公制单位", + "imperial": "英制单位" + } + }, + "review": { + "imageSource": { + "recordings": "录制文件", + "previews": "预览" + } + }, + "logger": { + "logLevel": { + "debug": "调试", + "info": "信息", + "warning": "警告", + "error": "错误", + "critical": "关键" + } + }, + "modelSize": { + "small": "小型", + "large": "大型" + }, + "retainMode": { + "all": "全部", + "motion": "运动", + "active_objects": "活动目标" + }, + "previewQuality": { + "very_high": "非常高", + "high": "高", + "medium": "中等", + "low": "低", + "very_low": "非常低" + }, + "detectorsAndModel": { + "title": "检测器和模型", + "description": "配置用于运行目标检测的检测器后端及对应模型,配置将统一保存,确保检测器与模型保持匹配一致。", + "cardTitles": { + "detector": "检测器硬件", + "model": "检测器模型" + }, + "tabs": { + "plus": "Frigate+", + "custom": "自定义模型" + }, + "mismatch": { + "warning": "当前 Frigate+ 模型“{{model}}”需搭配 {{required}} 检测器使用。请在下方选择兼容的模型,或切换为自定义模型后再保存。" + }, + "plusModel": { + "requiresDetector": "需要检测器:{{detector}}", + "noModelSelected": "选择 Frigate+ 模型" + }, + "toast": { + "saveSuccess": "检测器与模型设置已保存,请重启 Frigate 以生效配置。", + "saveError": "保存检测器及模型设置失败" + }, + "unsavedChanges": "检测器与模型配置存在未保存修改", + "restartRequired": "需要重启(检测器 或 模型 的设置已变更)" + }, + "menuDot": { + "overrideGlobal": "这一部分覆盖了全局配置", + "overrideProfile": "本节被 {{profile}} 配置文件覆盖", + "unsaved": "这一部分有未保存的更改" } } From 03f4f76b7285c2821d316430d53567c91b561b32 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 19 May 2026 22:17:04 +0200 Subject: [PATCH 27/94] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (1141 of 1141 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (794 of 794 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (50 of 50 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (473 of 473 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (127 of 127 strings) Update translation files Updated by "Squash Git commits" add-on in Weblate. Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (59 of 59 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (45 of 45 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (237 of 237 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (40 of 40 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (175 of 175 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (127 of 127 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (471 of 471 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (792 of 792 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (100 of 100 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (1122 of 1122 strings) Co-authored-by: Hosted Weblate Co-authored-by: OverTheHillsAndFarAway Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/nb_NO/ Translation: Frigate NVR/Config - Cameras Translation: Frigate NVR/Config - Global Translation: Frigate NVR/common Translation: Frigate NVR/objects Translation: Frigate NVR/views-chat Translation: Frigate NVR/views-facelibrary Translation: Frigate NVR/views-live Translation: Frigate NVR/views-motionSearch Translation: Frigate NVR/views-replay Translation: Frigate NVR/views-settings Translation: Frigate NVR/views-system --- web/public/locales/nb-NO/common.json | 6 +- web/public/locales/nb-NO/config/cameras.json | 12 +- web/public/locales/nb-NO/config/global.json | 14 +- web/public/locales/nb-NO/objects.json | 7 +- web/public/locales/nb-NO/views/chat.json | 65 +++++++- .../locales/nb-NO/views/faceLibrary.json | 6 +- web/public/locales/nb-NO/views/live.json | 3 +- .../locales/nb-NO/views/motionSearch.json | 76 ++++++++- web/public/locales/nb-NO/views/replay.json | 60 ++++++- web/public/locales/nb-NO/views/settings.json | 150 ++++++++++++++++-- web/public/locales/nb-NO/views/system.json | 3 + 11 files changed, 375 insertions(+), 27 deletions(-) diff --git a/web/public/locales/nb-NO/common.json b/web/public/locales/nb-NO/common.json index 411463881c..00fb832e3f 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -216,7 +216,8 @@ "gl": "Galego (Galisisk)", "id": "Bahasa Indonesia (Indonesisk)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Kroatisk)" + "hr": "Hrvatski (Kroatisk)", + "bs": "Bosanski (Bosnisk)" }, "appearance": "Utseende", "darkMode": { @@ -241,7 +242,8 @@ "classification": "Klassifisering", "profiles": "Profiler", "chat": "Chat", - "actions": "Handlinger" + "actions": "Handlinger", + "features": "Funksjoner" }, "pagination": { "next": { diff --git a/web/public/locales/nb-NO/config/cameras.json b/web/public/locales/nb-NO/config/cameras.json index d2a44f51e5..ce68cfc445 100644 --- a/web/public/locales/nb-NO/config/cameras.json +++ b/web/public/locales/nb-NO/config/cameras.json @@ -52,7 +52,7 @@ "description": "Innstillinger for å aktivere og kontrollere varslinger for dette kameraet." }, "audio": { - "label": "Lydhendelser", + "label": "Lyddeteksjon", "enabled": { "label": "Aktiver lyddeteksjon", "description": "Aktiver eller deaktiver deteksjon av lydhendelser for dette kameraet." @@ -71,7 +71,11 @@ }, "filters": { "label": "Lydfiltre", - "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive." + "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.", + "threshold": { + "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.", + "label": "Minimum konfidens for lyd" + } }, "enabled_in_config": { "label": "Opprinnelig lydstatus", @@ -476,6 +480,10 @@ "hwaccel_args": { "label": "Argumenter for maskinvareakselerasjon ved eksport", "description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding." + }, + "max_concurrent": { + "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", + "label": "Maksimalt antall samtidige eksporter" } }, "preview": { diff --git a/web/public/locales/nb-NO/config/global.json b/web/public/locales/nb-NO/config/global.json index 4c669b31db..11f2fbcdbf 100644 --- a/web/public/locales/nb-NO/config/global.json +++ b/web/public/locales/nb-NO/config/global.json @@ -242,7 +242,7 @@ "description": "Aktiver overvåking av nettverksbåndbredde per prosess for kamera-ffmpeg-prosesser og detektorer." }, "intel_gpu_device": { - "label": "SR-IOV-enhet", + "label": "Intel GPU-enhet", "description": "Enhetsidentifikator som brukes når Intel-GPU-er behandles som SR-IOV for å korrigere GPU-statistikk." } }, @@ -539,7 +539,7 @@ } }, "audio": { - "label": "Lydhendelser", + "label": "Lyddeteksjon", "description": "Innstillinger for lydbasert hendelsesdeteksjon for alle kameraer; kan overstyres per kamera.", "enabled": { "label": "Aktiver lyddeteksjon", @@ -559,7 +559,11 @@ }, "filters": { "label": "Lydfiltre", - "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive." + "description": "Filterinnstillinger per lydtype, som konfidensterskler for å redusere falske positive.", + "threshold": { + "description": "Minimum konfidens-terskel for at lydhendelsen skal bli registrert.", + "label": "Minimum konfidens for lyd" + } }, "enabled_in_config": { "label": "Opprinnelig lydstatus", @@ -1000,6 +1004,10 @@ "hwaccel_args": { "label": "Argumenter for maskinvareakselerasjon ved eksport", "description": "Argumenter for maskinvareakselerasjon som skal brukes ved eksport og transkoding." + }, + "max_concurrent": { + "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", + "label": "Maksimalt antall samtidige eksporter" } }, "preview": { diff --git a/web/public/locales/nb-NO/objects.json b/web/public/locales/nb-NO/objects.json index eb4b3ee36d..ab352714fc 100644 --- a/web/public/locales/nb-NO/objects.json +++ b/web/public/locales/nb-NO/objects.json @@ -121,5 +121,10 @@ "skunk": "Skunk", "school_bus": "Skolebuss", "royal_mail": "Royal Mail", - "canada_post": "Canada Post" + "canada_post": "Canada Post", + "baby_stroller": "Barnevogn", + "Rodent": "Gnager", + "baby": "Baby", + "rickshaw": "Rickshaw", + "rodent": "Gnager" } diff --git a/web/public/locales/nb-NO/views/chat.json b/web/public/locales/nb-NO/views/chat.json index 0967ef424b..a788c3635b 100644 --- a/web/public/locales/nb-NO/views/chat.json +++ b/web/public/locales/nb-NO/views/chat.json @@ -1 +1,64 @@ -{} +{ + "documentTitle": "Chat - Frigate", + "title": "Frigate Chat", + "subtitle": "Din AI-assistent for kamerahåndtering og innsikt", + "placeholder": "Spør om hva som helst...", + "error": "Noe gikk galt. Vennligst prøv igjen.", + "processing": "Behandler...", + "toolsUsed": "Brukt: {{tools}}", + "showTools": "Vis verktøy ({{count}})", + "hideTools": "Skjul verktøy", + "call": "Kall", + "result": "Resultat", + "arguments": "Argumenter:", + "response": "Svar:", + "attachment_chip_label": "{{label}} på {{camera}}", + "attachment_chip_remove": "Fjern vedlegg", + "open_in_explore": "Åpne i Utforsk", + "attach_event_aria": "Legg ved hendelse {{eventId}}", + "attachment_picker_paste_label": "Eller lim inn hendelses-ID", + "attachment_picker_attach": "Legg ved", + "attachment_picker_placeholder": "Legg ved en hendelse", + "quick_reply_find_similar": "Finn lignende observasjoner", + "quick_reply_tell_me_more": "Fortell meg mer om dette", + "quick_reply_when_else": "Når ellers ble det sett?", + "quick_reply_find_similar_text": "Finn lignende observasjoner som denne.", + "quick_reply_tell_me_more_text": "Fortell meg mer om denne.", + "quick_reply_when_else_text": "Når ellers ble denne sett?", + "anchor": "Referanse", + "similarity_score": "Likhet", + "no_similar_objects_found": "Ingen lignende objekter funnet.", + "semantic_search_required": "Semantisk søk må være aktivert for å finne lignende objekter.", + "send": "Send", + "suggested_requests": "Prøv å spørre:", + "starting_requests": { + "show_recent_events": "Vis nylige hendelser", + "show_camera_status": "Vis kamerastatus", + "recap": "Hva skjedde mens jeg var borte?", + "watch_camera": "Overvåk et kamera for aktivitet" + }, + "starting_requests_prompts": { + "show_recent_events": "Vis meg nylige hendelser fra den siste timen", + "show_camera_status": "Hva er status for kameraene mine akkurat nå?", + "recap": "Hva skjedde mens jeg var borte?", + "watch_camera": "Overvåk inngangsdøren og gi meg beskjed hvis noen dukker opp" + }, + "new_chat": "Ny chat", + "settings": { + "title": "Chat-innstillinger", + "show_stats": { + "title": "Vis statistikk", + "desc": "Vis genereringshastighet og kontekststørrelse for chat-svar.", + "while_generating": "Under generering", + "always": "Alltid" + }, + "auto_scroll": { + "title": "Auto-rulling", + "desc": "Følg nye meldinger etter hvert som de ankommer." + } + }, + "stats": { + "context": "{{tokens}} tokens", + "tokens_per_second": "{{rate}} t/s" + } +} diff --git a/web/public/locales/nb-NO/views/faceLibrary.json b/web/public/locales/nb-NO/views/faceLibrary.json index cf8d81e394..4ea7e819ff 100644 --- a/web/public/locales/nb-NO/views/faceLibrary.json +++ b/web/public/locales/nb-NO/views/faceLibrary.json @@ -27,7 +27,11 @@ "aria": "Velg nylige gjenkjennelser", "title": "Nylige gjenkjennelser", "empty": "Det er ingen nylige forsøk på ansiktsgjenkjenning", - "titleShort": "Nylige" + "titleShort": "Nylige", + "emptyNoLibrary": { + "description": "Du må legge til minst ett ansikt i biblioteket for at ansiktsgjenkjenning skal fungere.", + "title": "Last opp et ansikt" + } }, "selectFace": "Velg ansikt", "deleteFaceLibrary": { diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json index be891769e2..da219820f5 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -152,7 +152,8 @@ }, "recording": { "enable": "Aktiver opptak", - "disable": "Deaktiver opptak" + "disable": "Deaktiver opptak", + "disabledInConfig": "Opptak må først aktiveres i Innstillinger for dette kameraet." }, "streamStats": { "enable": "Vis Strømmestatistikk", diff --git a/web/public/locales/nb-NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json index 0967ef424b..c8fdb7c873 100644 --- a/web/public/locales/nb-NO/views/motionSearch.json +++ b/web/public/locales/nb-NO/views/motionSearch.json @@ -1 +1,75 @@ -{} +{ + "documentTitle": "Bevegelsessøk - Frigate", + "title": "Bevegelsessøk", + "description": "Tegn et polygon for å definere et interesseområde, og angi et tidsrom for å søke etter bevegelsesendringer i dette området.", + "selectCamera": "Bevegelsessøk laster", + "startSearch": "Start søk", + "searchStarted": "Søk startet", + "searchCancelled": "Søk avbrutt", + "cancelSearch": "Avbryt", + "searching": "Søk pågår...", + "searchComplete": "Søk fullført", + "noResultsYet": "Kjør et søk for å finne bevegelsesendringer i det valgte området", + "noChangesFound": "Ingen pikselendringer ble funnet i det valgte området", + "changesFound_one": "Fant {{count}} bevegelsesendring", + "changesFound_other": "Fant {{count}} bevegelsesendringer", + "framesProcessed": "{{count}} bilder behandlet", + "jumpToTime": "Gå til dette tidspunktet", + "results": "Resultater", + "showSegmentHeatmap": "Varmekart", + "newSearch": "Nytt søk", + "clearResults": "Tøm resultater", + "clearROI": "Slett polygon", + "polygonControls": { + "points_one": "{{count}} punkt", + "points_other": "{{count}} punkter", + "undo": "Angre siste punkt", + "reset": "Tilbakestill polygon" + }, + "motionHeatmapLabel": "Varmekart for bevegelse", + "dialog": { + "title": "Bevegelsessøk", + "cameraLabel": "Kamera", + "previewAlt": "Forhåndsvisning av kamera for {{camera}}" + }, + "timeRange": { + "title": "Søkeperiode", + "start": "Starttid", + "end": "Sluttid" + }, + "settings": { + "title": "Søkeinnstillinger", + "parallelMode": "Parallellmodus", + "parallelModeDesc": "Skann flere opptakssegmenter samtidig (raskere, men betydelig mer CPU-intensivt)", + "threshold": "Følsomhetsterskel", + "thresholdDesc": "Lavere verdier detekterer mindre endringer (1–255)", + "minArea": "Minimum endringsområde", + "minAreaDesc": "Minimum prosentandel av interesseområdet som må endres for å anses som betydelig", + "frameSkip": "Bilde-sprang", + "frameSkipDesc": "Behandle hvert N-te bilde. Sett denne til kameraets bildefrekvens for å behandle ett bilde i sekundet (f.eks. 5 for et 5 FPS-kamera, 30 for et 30 FPS-kamera). Høyere verdier vil være raskere, men kan gå glipp av korte bevegelseshendelser.", + "maxResults": "Maksimalt antall resultater", + "maxResultsDesc": "Stopp etter dette antallet samsvarende tidsstempler" + }, + "errors": { + "noCamera": "Vennligst velg et kamera", + "noROI": "Vennligst tegn et interesseområde", + "noTimeRange": "Vennligst velg en tidsperiode", + "invalidTimeRange": "Sluttid må være etter starttid", + "searchFailed": "Søk mislyktes: {{message}}", + "polygonTooSmall": "Polygonet må ha minst 3 punkter", + "unknown": "Ukjent feil" + }, + "changePercentage": "{{percentage}} % endret", + "metrics": { + "title": "Statistikk for søk", + "segmentsScanned": "Segmenter skannet", + "segmentsProcessed": "Behandlet", + "segmentsSkippedInactive": "Hoppet over (ingen aktivitet)", + "segmentsSkippedHeatmap": "Hoppet over (manglende ROI-overlapp)", + "fallbackFullRange": "Fullskanning som reserve", + "framesDecoded": "Bilder dekodet", + "wallTime": "Søketid", + "segmentErrors": "Segmentfeil", + "seconds": "{{seconds}}s" + } +} diff --git a/web/public/locales/nb-NO/views/replay.json b/web/public/locales/nb-NO/views/replay.json index 0967ef424b..3cf72e2011 100644 --- a/web/public/locales/nb-NO/views/replay.json +++ b/web/public/locales/nb-NO/views/replay.json @@ -1 +1,59 @@ -{} +{ + "title": "Feilsøkingsavspilling", + "description": "Spill av kameraopptak for feilsøking. Objektlisten viser et tidsforsinket sammendrag av detekterte objekter, og fanen Meldinger viser en strøm av Frigates interne meldinger fra avspillingen.", + "websocket_messages": "Meldinger", + "dialog": { + "title": "Start feilsøkingsavspilling", + "description": "Opprett et midlertidig avspillingskamera som repeterer historisk materiale for å feilsøke problemer med objektdeteksjon og sporing. Avspillingskameraet vil ha samme deteksjonskonfigurasjon som kildekameraet. Velg et tidsrom for å begynne.", + "camera": "Kildekamera", + "timeRange": "Tidsrom", + "preset": { + "1m": "Siste minutt", + "5m": "Siste 5 minutter", + "timeline": "Fra tidslinje", + "custom": "Egendefinert" + }, + "startButton": "Start avspilling", + "selectFromTimeline": "Velg", + "starting": "Starter avspilling...", + "startLabel": "Start", + "endLabel": "Slutt", + "toast": { + "error": "Kunne ikke starte feilsøkingsavspilling: {{error}}", + "alreadyActive": "En avspillingsøkt er allerede aktiv", + "stopError": "Kunne ikke stoppe feilsøkingsavspilling: {{error}}", + "goToReplay": "Gå til avspilling" + } + }, + "page": { + "noSession": "Ingen aktiv feilsøkingsøkt", + "noSessionDesc": "Start en feilsøkingsavspilling fra Historikk-visningen ved å klikke på Handlinger-knappen i verktøylinjen og velge Feilsøkingsavspilling.", + "goToRecordings": "Gå til historikk", + "preparingClip": "Forbereder klipp…", + "preparingClipDesc": "Frigate syr sammen opptak for det valgte tidsrommet. Dette kan ta et minutt for lengre perioder.", + "startingCamera": "Starter feilsøkingsavspilling…", + "startError": { + "title": "Kunne ikke starte feilsøkingsavspilling", + "back": "Tilbake til historikk" + }, + "sourceCamera": "Kildekamera", + "replayCamera": "Avspillingskamera", + "initializingReplay": "Initialiserer feilsøkingsavspilling...", + "stoppingReplay": "Stopper feilsøkingsavspilling...", + "stopReplay": "Stopp avspilling", + "confirmStop": { + "title": "Stoppe feilsøkingsavspilling?", + "description": "Dette vil stoppe økten og slette alle midlertidige data. Er du sikker?", + "confirm": "Stopp avspilling", + "cancel": "Avbryt" + }, + "activity": "Aktivitet", + "objects": "Objektliste", + "audioDetections": "Lyd-deteksjoner", + "noActivity": "Ingen aktivitet detektert", + "activeTracking": "Aktiv sporing", + "noActiveTracking": "Ingen aktiv sporing", + "configuration": "Konfigurasjon", + "configurationDesc": "Finjuster innstillinger for bevegelsesdeteksjon og objektsporing for feilsøkingskameraet. Ingen endringer lagres i din Frigate-konfigurasjonsfil." + } +} diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json index 6d907f91ea..69055fd4bc 100644 --- a/web/public/locales/nb-NO/views/settings.json +++ b/web/public/locales/nb-NO/views/settings.json @@ -61,8 +61,8 @@ "cameraLpr": "Kjennemerke-gjenkjenning", "integrationLpr": "Kjennemerke-gjenkjenning", "systemLogging": "Logging", - "cameraAudioEvents": "Lydhendelser", - "globalAudioEvents": "Lydhendelser", + "cameraAudioEvents": "Lyd-deteksjon", + "globalAudioEvents": "Lyd-deteksjon", "cameraAudioTranscription": "Lydtranskripsjon", "integrationAudioTranscription": "Lydtranskripsjon", "systemDetectorHardware": "Maskinvare for detektor", @@ -791,6 +791,14 @@ "plusModelType": { "userModel": "Finjustert", "baseModel": "Basismodell" + }, + "noModelLoaded": "Ingen Frigate+-modell er lastet inn for øyeblikket.", + "selectModel": "Velg en modell", + "noModelsAvailable": "Ingen modeller tilgjengelig", + "filter": { + "ariaLabel": "Filtrer modeller etter type", + "baseModels": "Basismodeller", + "fineTunedModels": "Finjusterte modeller" } }, "title": "Frigate+ Innstillinger", @@ -1341,7 +1349,8 @@ }, "hikvision": { "substreamWarning": "Substrøm 1 er låst til lav oppløsning. Mange Hikvision-kameraer støtter flere substrømmer som må aktiveres i kameraets innstillinger. Det anbefales å sjekke og benytte disse strømmene hvis de er tilgjengelige." - } + }, + "resolutionUnknown": "Oppløsningen for denne strømmen kunne ikke fastslås. Du bør angi deteksjonsoppløsningen manuelt i innstillingene eller i konfigurasjonen din." } } }, @@ -1358,7 +1367,13 @@ "enableSuccess": "Aktiverte {{cameraName}} i konfigurasjonen. Start Frigate på nytt for å ta i bruk endringene.", "enableLabel": "Aktiverte kameraer", "enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig.
    Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc.", - "disableLabel": "Deaktiverte kameraer" + "disableLabel": "Deaktiverte kameraer", + "friendlyName": { + "edit": "Rediger visningsnavn for kamera", + "title": "Rediger visningsnavn", + "description": "Angi visningsnavnet som skal brukes for dette kameraet i Frigate-grensesnittet. La feltet stå tomt for å bruke kamera-ID.", + "rename": "Omdøp" + } }, "cameraConfig": { "add": "Legg til kamera", @@ -1408,7 +1423,16 @@ "description": "Sletting av et kamera vil fjerne alle opptak, sporede objekter og konfigurasjon for dette kameraet permanent. Eventuelle go2rtc-strømmer tilknyttet kameraet må eventuelt fjernes manuelt.", "selectPlaceholder": "Velg kamera..." }, - "deleteCamera": "Slett kamera" + "deleteCamera": "Slett kamera", + "cameraType": { + "title": "Kameratype", + "label": "Kameratype", + "description": "Angi type for hvert kamera. Dedikerte LPR-kameraer er spesialkameraer med kraftig optisk zoom for å fange opp kjennemerker på kjøretøy langt unna. De fleste kameraer bør bruke typen \"Normal\", med mindre kameraet er spesifikt for gjenkjenning av kjennemerker og har et snevert fokus på kjennemerker.", + "normal": "Normal", + "dedicatedLpr": "Dedikert LPR (lesing av kjennemerker)", + "saveSuccess": "Kameratype oppdatert for {{cameraName}}. Start Frigate på nytt for å bruke endringene." + }, + "description": "Legg til, rediger og slett kameraer, kontroller hvilke kameraer som er aktivert, og konfigurer overstyringer for hver profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon." }, "cameraReview": { "title": "Innstillinger for kamerainspeksjon", @@ -1572,7 +1596,9 @@ "options": { "embeddings": "Vektorrepresentasjoner", "tools": "Verktøy", - "vision": "Bildegjenkjenning" + "vision": "Bildegjenkjenning", + "descriptions": "Beskrivelser", + "chat": "Chat" } }, "additionalProperties": { @@ -1645,7 +1671,24 @@ "overriddenBaseConfigTooltip": "{{profile}}-profilen overstyrer konfigurasjonsinnstillinger i denne seksjonen", "overriddenGlobalTooltip": "Dette kameraet overstyrer globale konfigurasjonsinnstillinger i denne seksjonen", "overriddenBaseConfig": "Overstyrt (Basiskonfigurasjon)", - "overriddenGlobal": "Overstyrt (Global)" + "overriddenGlobal": "Overstyrt (Global)", + "overriddenInCameras": { + "label_one": "Overstyrt i {{count}} kamera", + "label_other": "Overstyrt i {{count}} kameraer", + "tooltip_one": "{{count}} kamera overstyrer verdier i denne seksjonen. Klikk for å se detaljer.", + "tooltip_other": "{{count}} kameraer overstyrer verdier i denne seksjonen. Klikk for å se detaljer.", + "heading_one": "Denne globale seksjonen har felt som er overstyrt i {{count}} kamera.", + "heading_other": "Denne globale seksjonen har felt som er overstyrt i {{count}} kameraer.", + "othersField_one": "{{count}} annen", + "othersField_other": "{{count}} andre", + "profilePrefix": "{{profile}}-profil: {{fields}}" + }, + "overriddenGlobalHeading_one": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:", + "overriddenGlobalHeading_other": "Dette kameraet overstyrer {{count}} felt fra den globale konfigurasjonen:", + "overriddenGlobalNoDeltas": "Dette kameraet overstyrer den globale konfigurasjonen, men ingen feltverdier er forskjellige.", + "overriddenBaseConfigHeading_one": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:", + "overriddenBaseConfigHeading_other": "{{profile}}-profilen overstyrer {{count}} felt fra basiskonfigurasjonen:", + "overriddenBaseConfigNoDeltas": "{{profile}}-profilen overstyrer denne seksjonen, men ingen feltverdier er forskjellige fra basiskonfigurasjonen." }, "detectionModel": { "plusActive": { @@ -1744,18 +1787,21 @@ "review": { "allNonAlertDetections": "All aktivitet som ikke er et varsel, vil bli inkludert som deteksjoner.", "detectDisabled": "Objektdeteksjon er deaktivert. Inspeksjonselementer krever detekterte objekter for å kategorisere varsler og deteksjoner.", - "recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert." + "recordDisabled": "Opptak er deaktivert, inspeksjonselementer vil ikke bli generert.", + "genaiImageSourceRecordingsRecordDisabled": "Bildekilde er satt til \"opptak\", men opptak er deaktivert. Frigate vil falle tilbake til forhåndsvisningsbilder." }, "detectors": { "mixedTypesSuggestion": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer eller velg {{type}}.", "mixedTypes": "Alle detektorer må bruke samme type. Fjern eksisterende detektorer for å bruke en annen type." }, "faceRecognition": { - "globalDisabled": "Ansiktsgjenkjenning er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at ansiktsgjenkjenning på kameranivå skal fungere.", - "personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Sørg for at 'person' er i listen over objektsporing." + "globalDisabled": "Utvidelse for ansiktsgjenkjenning må være aktivert for at funksjoner for ansiktsgjenkjenning skal fungere på dette kameraet.", + "personNotTracked": "Ansiktsgjenkjenning krever at objektet 'person' spores. Aktiver 'person' under Objekter for dette kameraet.", + "modelSizeLarge": "Den store (large) modellen krever GPU eller NPU for akseptabel ytelse. Bruk liten (small) på systemer med kun CPU." }, "detect": { - "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5." + "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5. Høyere verdier kan føre til ytelsesproblemer uten å gi noen fordeler.", + "disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere." }, "birdseye": { "objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv." @@ -1773,8 +1819,15 @@ "detectDisabled": "Objektdeteksjon er deaktivert. Stillbilder genereres fra sporede objekter og vil ikke bli opprettet." }, "lpr": { - "globalDisabled": "Identifisering av kjennemerker er ikke aktivert på globalt nivå. Aktiver det i globale innstillinger for at identifisering på kameranivå skal fungere.", - "vehicleNotTracked": "Identifisering av kjnnemerker krever at 'bil' eller 'motorsykkel' spores." + "globalDisabled": "Utvidelse for identifisering av kjennemerker må være aktivert for at LPR-funksjoner skal fungere på dette kameraet.", + "vehicleNotTracked": "Identifisering av kjennemerker krever at 'bil' eller 'motorsykkel' spores. Aktiver 'bil' eller 'motorsykkel' under Objekter for dette kameraet.", + "modelSizeLarge": "Den store (large) modellen er optimalisert for kjennemerker over flere linjer. Den lille (small) modellen gir bedre ytelse og bør brukes med mindre din region bruker skiltformater med flere linjer." + }, + "objects": { + "genaiNoDescriptionsProvider": "Du må konfigurere en GenAI-leverandør med rollen \"beskrivelser\" for at beskrivelser skal kunne genereres." + }, + "semanticSearch": { + "jinav2SmallModelSize": "Størrelsen \"liten\" med Jina V2-modellen har høyt minnebruk og beregningskostnad. Den \"store\" modellen med en dedikert GPU anbefales." } }, "maintenance": { @@ -1839,7 +1892,14 @@ }, "onvif": { "profileAuto": "Auto", - "profileLoading": "Laster profiler..." + "profileLoading": "Laster profiler...", + "autotracking": { + "zooming": { + "disabled": "Deaktivert", + "absolute": "Absolutt", + "relative": "Relativ" + } + } }, "confirmReset": "Bekreft nullstilling", "resetToDefaultDescription": "Dette vil nullstille alle innstillinger i denne seksjonen til standardverdiene. Denne handlingen kan ikke angres.", @@ -1903,5 +1963,67 @@ "bl": "Nederst til venstre", "tr": "Øverst til høyre", "tl": "Øverst til venstre" + }, + "birdseye": { + "trackingMode": { + "objects": "Objekter", + "motion": "Bevegelse", + "continuous": "Kontinuerlig" + } + }, + "snapshot": { + "retainMode": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + } + }, + "ui": { + "timeFormat": { + "browser": "Nettleser", + "12hour": "12 timer", + "24hour": "24 timer" + }, + "TimeOrDateStyle": { + "full": "Full", + "long": "Lang", + "medium": "Middels", + "short": "Kort" + }, + "unitSystem": { + "metric": "Metrisk", + "imperial": "Imperial" + } + }, + "review": { + "imageSource": { + "recordings": "Opptak", + "previews": "Forhåndsvisninger" + } + }, + "logger": { + "logLevel": { + "debug": "Debug", + "info": "Info", + "warning": "Advarsel", + "error": "Feil", + "critical": "Kritisk" + } + }, + "modelSize": { + "small": "Liten", + "large": "Stor" + }, + "retainMode": { + "all": "Alle", + "motion": "Bevegelse", + "active_objects": "Aktive objekter" + }, + "previewQuality": { + "very_high": "Svært høy", + "high": "Høy", + "medium": "Middels", + "low": "Lav", + "very_low": "Svært lav" } } diff --git a/web/public/locales/nb-NO/views/system.json b/web/public/locales/nb-NO/views/system.json index 374e6457b6..ef3ca18e1e 100644 --- a/web/public/locales/nb-NO/views/system.json +++ b/web/public/locales/nb-NO/views/system.json @@ -210,6 +210,9 @@ "expectedFps": "Forventet BPS", "reconnectsLastHour": "Gjentatte tilkoblinger (siste time)", "stallsLastHour": "Avbrudd (siste time)" + }, + "noCameras": { + "title": "Ingen kameraer funnet" } }, "enrichments": { From 8ea46e7c6c195483320039d93fd62ee0c68cf86d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 May 2026 09:36:49 -0500 Subject: [PATCH 28/94] Miscellaneous fixes (#23258) * render orphaned filter entries as collapsibles instead of the Key/Value editor * Symlink for various AI files * change replay confg dialog to platform aware sheet * change agents title * fix test * tweak collapsible * remove camera ui section in settings no point to having it anymore with profiles and camera management settings * fix admin response cache leak to non-admin users via nginx proxy_cache * add model fetcher endpoint for genai config ui --------- Co-authored-by: Nicolas Mowen --- .github/copilot-instructions.md | 440 +----------------- AGENTS.md | 439 +++++++++++++++++ CLAUDE.md | 1 + .../rootfs/usr/local/nginx/conf/nginx.conf | 1 + docs/static/frigate-api.yaml | 74 +++ frigate/api/app.py | 101 +++- frigate/api/defs/request/app_body.py | 9 + frigate/config/camera/genai.py | 2 +- frigate/genai/__init__.py | 8 +- frigate/genai/plugins/llama_cpp.py | 4 + frigate/genai/plugins/ollama.py | 3 + frigate/test/http_api/test_http_app.py | 96 +++- web/e2e/specs/replay.spec.ts | 10 +- web/public/locales/en/views/settings.json | 11 +- .../config-form/sections/BaseSection.tsx | 12 +- .../sections/section-special-cases.ts | 46 +- .../theme/widgets/GenAIModelWidget.tsx | 325 ++++++++++--- web/src/pages/Replay.tsx | 118 ++--- web/src/pages/Settings.tsx | 7 - 19 files changed, 1111 insertions(+), 596 deletions(-) mode change 100644 => 120000 .github/copilot-instructions.md create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index d87dbb239e..0000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,439 +0,0 @@ -# GitHub Copilot Instructions for Frigate NVR - -This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection. - -## Project Overview - -Frigate NVR is a realtime object detection system for IP cameras that uses: - -- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX -- **Frontend**: React with TypeScript, Vite, TailwindCSS -- **Architecture**: Multiprocessing design with ZMQ and MQTT communication -- **Focus**: Minimal resource usage with maximum performance - -## Code Review Guidelines - -When reviewing code, do NOT comment on: - -- Missing imports - Static analysis tooling catches these -- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting -- Minor style inconsistencies already enforced by linters - -## Python Backend Standards - -### Python Requirements - -- **Compatibility**: Python 3.13+ -- **Language Features**: Use modern Python features: - - Pattern matching - - Type hints (comprehensive typing preferred) - - f-strings (preferred over `%` or `.format()`) - - Dataclasses - - Async/await patterns - -### Code Quality Standards - -- **Formatting**: Ruff (configured in `pyproject.toml`) -- **Linting**: Ruff with rules defined in project config -- **Type Checking**: Use type hints consistently -- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests -- **Language**: American English for all code, comments, and documentation - -### Logging Standards - -- **Logger Pattern**: Use module-level logger - - ```python - import logging - - logger = logging.getLogger(__name__) - ``` - -- **Format Guidelines**: - - No periods at end of log messages - - No sensitive data (keys, tokens, passwords) - - Use lazy logging: `logger.debug("Message with %s", variable)` -- **Log Levels**: - - `debug`: Development and troubleshooting information - - `info`: Important runtime events (startup, shutdown, state changes) - - `warning`: Recoverable issues that should be addressed - - `error`: Errors that affect functionality but don't crash the app - - `exception`: Use in except blocks to include traceback - -### Error Handling - -- **Exception Types**: Choose most specific exception available -- **Try/Catch Best Practices**: - - Only wrap code that can throw exceptions - - Keep try blocks minimal - process data after the try/except - - Avoid bare exceptions except in background tasks - - Bad pattern: - - ```python - try: - data = await device.get_data() # Can throw - # ❌ Don't process data inside try block - processed = data.get("value", 0) * 100 - result = processed - except DeviceError: - logger.error("Failed to get data") - ``` - - Good pattern: - - ```python - try: - data = await device.get_data() # Can throw - except DeviceError: - logger.error("Failed to get data") - return - - # ✅ Process data outside try block - processed = data.get("value", 0) * 100 - result = processed - ``` - -### Async Programming - -- **External I/O**: All external I/O operations must be async -- **Best Practices**: - - Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()` - - Avoid awaiting in loops - use `asyncio.gather()` instead - - No blocking calls in async functions - - Use `asyncio.create_task()` for background operations -- **Thread Safety**: Use proper synchronization for shared state - -### Documentation Standards - -- **Module Docstrings**: Concise descriptions at top of files - ```python - """Utilities for motion detection and analysis.""" - ``` -- **Function Docstrings**: Required for public functions and methods - - ```python - async def process_frame(frame: ndarray, config: Config) -> Detection: - """Process a video frame for object detection. - - Args: - frame: The video frame as numpy array - config: Detection configuration - - Returns: - Detection results with bounding boxes - """ - ``` - -- **Comment Style**: - - Explain the "why" not just the "what" - - Keep lines under 88 characters when possible - - Use clear, descriptive comments - -### File Organization - -- **API Endpoints**: `frigate/api/` - FastAPI route handlers -- **Configuration**: `frigate/config/` - Configuration parsing and validation -- **Detectors**: `frigate/detectors/` - Object detection backends -- **Events**: `frigate/events/` - Event management and storage -- **Utilities**: `frigate/util/` - Shared utility functions - -## Frontend (React/TypeScript) Standards - -### Internationalization (i18n) - -- **CRITICAL**: Never write user-facing strings directly in components -- **Always use react-i18next**: Import and use the `t()` function - - ```tsx - import { useTranslation } from "react-i18next"; - - function MyComponent() { - const { t } = useTranslation(["views/live"]); - return
    {t("camera_not_found")}
    ; - } - ``` - -- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en` -- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`) - -### Code Quality - -- **Linting**: ESLint (see `web/.eslintrc.cjs`) -- **Formatting**: Prettier with Tailwind CSS plugin -- **Type Safety**: TypeScript strict mode enabled - -### Component Patterns - -- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`) -- **Styling**: TailwindCSS with `cn()` utility for class merging -- **State Management**: React hooks (useState, useEffect, useCallback, useMemo) -- **Data Fetching**: Custom hooks with proper loading and error states - -### ESLint Rules - -Key rules enforced: - -- `react-hooks/rules-of-hooks`: error -- `react-hooks/exhaustive-deps`: error -- `no-console`: error (use proper logging or remove) -- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`) -- Unused variables must be prefixed with `_` -- Comma dangles required for multiline objects/arrays - -### File Organization - -- **Pages**: `web/src/pages/` - Route components -- **Views**: `web/src/views/` - Complex view components -- **Components**: `web/src/components/` - Reusable components -- **Hooks**: `web/src/hooks/` - Custom React hooks -- **API**: `web/src/api/` - API client functions -- **Types**: `web/src/types/` - TypeScript type definitions - -## Testing Requirements - -### Backend Testing - -- **Framework**: Python unittest -- **Run Command**: `python3 -u -m unittest` -- **Location**: `frigate/test/` -- **Coverage**: Aim for comprehensive test coverage of core functionality -- **Pattern**: Use `TestCase` classes with descriptive test method names - ```python - class TestMotionDetection(unittest.TestCase): - def test_detects_motion_above_threshold(self): - # Test implementation - ``` - -### Test Best Practices - -- Always have a way to test your work and confirm your changes -- Write tests for bug fixes to prevent regressions -- Test edge cases and error conditions -- Mock external dependencies (cameras, APIs, hardware) -- Use fixtures for test data - -## Development Commands - -### Python Backend - -```bash -# Run all tests -python3 -u -m unittest - -# Run specific test file -python3 -u -m unittest frigate.test.test_ffmpeg_presets - -# Check formatting (Ruff) -ruff format --check frigate/ - -# Apply formatting -ruff format frigate/ - -# Run linter -ruff check frigate/ - -# Type check -python3 -u -m mypy --config-file frigate/mypy.ini frigate -``` - -### Frontend (from web/ directory) - -```bash -# Start dev server (AI agents should never run this directly unless asked) -npm run dev - -# Build for production -npm run build - -# Run linter -npm run lint - -# Fix linting issues -npm run lint:fix - -# Format code -npm run prettier:write - -# E2E: first-time setup -npm install -npx playwright install chromium - -# E2E: build the app and run all tests -npm run e2e:build && npm run e2e - -# E2E: interactive UI for debugging -npm run e2e:ui - -# E2E: run a specific spec -npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts - -# E2E: filter by name, or run only desktop/mobile -npx playwright test --config e2e/playwright.config.ts --grep="severity tab" -npx playwright test --config e2e/playwright.config.ts --project=desktop - -# E2E: regenerate mock data after backend model changes (from repo root) -PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py - -# Regenerate config translations from Pydantic models — outputs to -# web/public/locales/en/config/{global,cameras}.json. NEVER edit those -# JSON files by hand; change the Pydantic field title/description and -# re-run this script. (from repo root) -python3 generate_config_translations.py - -# Extract i18n keys from source into the locale files after adding -# new t() calls. Use the :ci variant to verify the locale files are -# in sync with source (fails if extraction would change anything). -npm run i18n:extract -npm run i18n:extract:ci -``` - -### Docker Development - -AI agents should never run these commands directly unless instructed. - -```bash -# Build local image -make local - -# Build debug image -make debug -``` - -## Common Patterns - -### API Endpoint Pattern - -```python -from fastapi import APIRouter, Request -from frigate.api.defs.tags import Tags - -router = APIRouter(tags=[Tags.Events]) - -@router.get("/events") -async def get_events(request: Request, limit: int = 100): - """Retrieve events from the database.""" - # Implementation -``` - -### Configuration Access - -```python -# Access Frigate configuration -config: FrigateConfig = request.app.frigate_config -camera_config = config.cameras["front_door"] -``` - -### Database Queries - -```python -from frigate.models import Event - -# Use Peewee ORM for database access -events = ( - Event.select() - .where(Event.camera == camera_name) - .order_by(Event.start_time.desc()) - .limit(limit) -) -``` - -## Common Anti-Patterns to Avoid - -### ❌ Avoid These - -```python -# Blocking operations in async functions -data = requests.get(url) # ❌ Use async HTTP client -time.sleep(5) # ❌ Use asyncio.sleep() - -# Hardcoded strings in React components -
    Camera not found
    # ❌ Use t("camera_not_found") - -# Missing error handling -data = await api.get_data() # ❌ No exception handling - -# Bare exceptions in regular code -try: - value = await sensor.read() -except Exception: # ❌ Too broad - logger.error("Failed") - -# Returning exceptions in JSON responses -except ValueError as e: - return JSONResponse( - content={"success": False, "message": str(e)}, - ) -``` - -### ✅ Use These Instead - -```python -# Async operations -import aiohttp -async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - data = await response.json() - -await asyncio.sleep(5) # ✅ Non-blocking - -# Translatable strings in React -const { t } = useTranslation(); -
    {t("camera_not_found")}
    # ✅ Translatable - -# Proper error handling -try: - data = await api.get_data() -except ApiException as err: - logger.error("API error: %s", err) - raise - -# Specific exceptions -try: - value = await sensor.read() -except SensorException as err: # ✅ Specific - logger.exception("Failed to read sensor") - -# Safe error responses -except ValueError: - logger.exception("Invalid parameters for API request") - return JSONResponse( - content={ - "success": False, - "message": "Invalid request parameters", - }, - ) -``` - -## WebSocket Broadcasts - -Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear. - -## Project-Specific Conventions - -### Configuration Files - -- Main config: `config/config.yml` - -### Directory Structure - -- Backend code: `frigate/` -- Frontend code: `web/` -- Docker files: `docker/` -- Documentation: `docs/` -- Database migrations: `migrations/` - -### Code Style Conformance - -Always conform new and refactored code to the existing coding style in the project: - -- Follow established patterns in similar files -- Match indentation and formatting of surrounding code -- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript) -- Maintain the same level of verbosity in comments and docstrings - -## Additional Resources - -- Documentation: https://docs.frigate.video -- Main Repository: https://github.com/blakeblackshear/frigate -- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..61b8373a82 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,439 @@ +# Agent Instructions for Frigate NVR + +This document provides coding guidelines and best practices for contributing to Frigate NVR, a complete and local NVR designed for Home Assistant with AI object detection. + +## Project Overview + +Frigate NVR is a realtime object detection system for IP cameras that uses: + +- **Backend**: Python 3.13+ with FastAPI, OpenCV, TensorFlow/ONNX +- **Frontend**: React with TypeScript, Vite, TailwindCSS +- **Architecture**: Multiprocessing design with ZMQ and MQTT communication +- **Focus**: Minimal resource usage with maximum performance + +## Code Review Guidelines + +When reviewing code, do NOT comment on: + +- Missing imports - Static analysis tooling catches these +- Code formatting - Ruff (Python) and Prettier (TypeScript/React) handle formatting +- Minor style inconsistencies already enforced by linters + +## Python Backend Standards + +### Python Requirements + +- **Compatibility**: Python 3.13+ +- **Language Features**: Use modern Python features: + - Pattern matching + - Type hints (comprehensive typing preferred) + - f-strings (preferred over `%` or `.format()`) + - Dataclasses + - Async/await patterns + +### Code Quality Standards + +- **Formatting**: Ruff (configured in `pyproject.toml`) +- **Linting**: Ruff with rules defined in project config +- **Type Checking**: Use type hints consistently +- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests +- **Language**: American English for all code, comments, and documentation + +### Logging Standards + +- **Logger Pattern**: Use module-level logger + + ```python + import logging + + logger = logging.getLogger(__name__) + ``` + +- **Format Guidelines**: + - No periods at end of log messages + - No sensitive data (keys, tokens, passwords) + - Use lazy logging: `logger.debug("Message with %s", variable)` +- **Log Levels**: + - `debug`: Development and troubleshooting information + - `info`: Important runtime events (startup, shutdown, state changes) + - `warning`: Recoverable issues that should be addressed + - `error`: Errors that affect functionality but don't crash the app + - `exception`: Use in except blocks to include traceback + +### Error Handling + +- **Exception Types**: Choose most specific exception available +- **Try/Catch Best Practices**: + - Only wrap code that can throw exceptions + - Keep try blocks minimal - process data after the try/except + - Avoid bare exceptions except in background tasks + + Bad pattern: + + ```python + try: + data = await device.get_data() # Can throw + # ❌ Don't process data inside try block + processed = data.get("value", 0) * 100 + result = processed + except DeviceError: + logger.error("Failed to get data") + ``` + + Good pattern: + + ```python + try: + data = await device.get_data() # Can throw + except DeviceError: + logger.error("Failed to get data") + return + + # ✅ Process data outside try block + processed = data.get("value", 0) * 100 + result = processed + ``` + +### Async Programming + +- **External I/O**: All external I/O operations must be async +- **Best Practices**: + - Avoid sleeping in loops - use `asyncio.sleep()` not `time.sleep()` + - Avoid awaiting in loops - use `asyncio.gather()` instead + - No blocking calls in async functions + - Use `asyncio.create_task()` for background operations +- **Thread Safety**: Use proper synchronization for shared state + +### Documentation Standards + +- **Module Docstrings**: Concise descriptions at top of files + ```python + """Utilities for motion detection and analysis.""" + ``` +- **Function Docstrings**: Required for public functions and methods + + ```python + async def process_frame(frame: ndarray, config: Config) -> Detection: + """Process a video frame for object detection. + + Args: + frame: The video frame as numpy array + config: Detection configuration + + Returns: + Detection results with bounding boxes + """ + ``` + +- **Comment Style**: + - Explain the "why" not just the "what" + - Keep lines under 88 characters when possible + - Use clear, descriptive comments + +### File Organization + +- **API Endpoints**: `frigate/api/` - FastAPI route handlers +- **Configuration**: `frigate/config/` - Configuration parsing and validation +- **Detectors**: `frigate/detectors/` - Object detection backends +- **Events**: `frigate/events/` - Event management and storage +- **Utilities**: `frigate/util/` - Shared utility functions + +## Frontend (React/TypeScript) Standards + +### Internationalization (i18n) + +- **CRITICAL**: Never write user-facing strings directly in components +- **Always use react-i18next**: Import and use the `t()` function + + ```tsx + import { useTranslation } from "react-i18next"; + + function MyComponent() { + const { t } = useTranslation(["views/live"]); + return
    {t("camera_not_found")}
    ; + } + ``` + +- **Translation Files**: Add English strings to the appropriate json files in `web/public/locales/en` +- **Namespaces**: Organize translations by feature/view (e.g., `views/live`, `common`, `views/system`) + +### Code Quality + +- **Linting**: ESLint (see `web/.eslintrc.cjs`) +- **Formatting**: Prettier with Tailwind CSS plugin +- **Type Safety**: TypeScript strict mode enabled + +### Component Patterns + +- **UI Components**: Use Radix UI primitives (in `web/src/components/ui/`) +- **Styling**: TailwindCSS with `cn()` utility for class merging +- **State Management**: React hooks (useState, useEffect, useCallback, useMemo) +- **Data Fetching**: Custom hooks with proper loading and error states + +### ESLint Rules + +Key rules enforced: + +- `react-hooks/rules-of-hooks`: error +- `react-hooks/exhaustive-deps`: error +- `no-console`: error (use proper logging or remove) +- `@typescript-eslint/no-explicit-any`: warn (always use proper types instead of `any`) +- Unused variables must be prefixed with `_` +- Comma dangles required for multiline objects/arrays + +### File Organization + +- **Pages**: `web/src/pages/` - Route components +- **Views**: `web/src/views/` - Complex view components +- **Components**: `web/src/components/` - Reusable components +- **Hooks**: `web/src/hooks/` - Custom React hooks +- **API**: `web/src/api/` - API client functions +- **Types**: `web/src/types/` - TypeScript type definitions + +## Testing Requirements + +### Backend Testing + +- **Framework**: Python unittest +- **Run Command**: `python3 -u -m unittest` +- **Location**: `frigate/test/` +- **Coverage**: Aim for comprehensive test coverage of core functionality +- **Pattern**: Use `TestCase` classes with descriptive test method names + ```python + class TestMotionDetection(unittest.TestCase): + def test_detects_motion_above_threshold(self): + # Test implementation + ``` + +### Test Best Practices + +- Always have a way to test your work and confirm your changes +- Write tests for bug fixes to prevent regressions +- Test edge cases and error conditions +- Mock external dependencies (cameras, APIs, hardware) +- Use fixtures for test data + +## Development Commands + +### Python Backend + +```bash +# Run all tests +python3 -u -m unittest + +# Run specific test file +python3 -u -m unittest frigate.test.test_ffmpeg_presets + +# Check formatting (Ruff) +ruff format --check frigate/ + +# Apply formatting +ruff format frigate/ + +# Run linter +ruff check frigate/ + +# Type check +python3 -u -m mypy --config-file frigate/mypy.ini frigate +``` + +### Frontend (from web/ directory) + +```bash +# Start dev server (AI agents should never run this directly unless asked) +npm run dev + +# Build for production +npm run build + +# Run linter +npm run lint + +# Fix linting issues +npm run lint:fix + +# Format code +npm run prettier:write + +# E2E: first-time setup +npm install +npx playwright install chromium + +# E2E: build the app and run all tests +npm run e2e:build && npm run e2e + +# E2E: interactive UI for debugging +npm run e2e:ui + +# E2E: run a specific spec +npx playwright test --config e2e/playwright.config.ts e2e/specs/live.spec.ts + +# E2E: filter by name, or run only desktop/mobile +npx playwright test --config e2e/playwright.config.ts --grep="severity tab" +npx playwright test --config e2e/playwright.config.ts --project=desktop + +# E2E: regenerate mock data after backend model changes (from repo root) +PYTHONPATH=. python3 web/e2e/fixtures/mock-data/generate-mock-data.py + +# Regenerate config translations from Pydantic models — outputs to +# web/public/locales/en/config/{global,cameras}.json. NEVER edit those +# JSON files by hand; change the Pydantic field title/description and +# re-run this script. (from repo root) +python3 generate_config_translations.py + +# Extract i18n keys from source into the locale files after adding +# new t() calls. Use the :ci variant to verify the locale files are +# in sync with source (fails if extraction would change anything). +npm run i18n:extract +npm run i18n:extract:ci +``` + +### Docker Development + +AI agents should never run these commands directly unless instructed. + +```bash +# Build local image +make local + +# Build debug image +make debug +``` + +## Common Patterns + +### API Endpoint Pattern + +```python +from fastapi import APIRouter, Request +from frigate.api.defs.tags import Tags + +router = APIRouter(tags=[Tags.Events]) + +@router.get("/events") +async def get_events(request: Request, limit: int = 100): + """Retrieve events from the database.""" + # Implementation +``` + +### Configuration Access + +```python +# Access Frigate configuration +config: FrigateConfig = request.app.frigate_config +camera_config = config.cameras["front_door"] +``` + +### Database Queries + +```python +from frigate.models import Event + +# Use Peewee ORM for database access +events = ( + Event.select() + .where(Event.camera == camera_name) + .order_by(Event.start_time.desc()) + .limit(limit) +) +``` + +## Common Anti-Patterns to Avoid + +### ❌ Avoid These + +```python +# Blocking operations in async functions +data = requests.get(url) # ❌ Use async HTTP client +time.sleep(5) # ❌ Use asyncio.sleep() + +# Hardcoded strings in React components +
    Camera not found
    # ❌ Use t("camera_not_found") + +# Missing error handling +data = await api.get_data() # ❌ No exception handling + +# Bare exceptions in regular code +try: + value = await sensor.read() +except Exception: # ❌ Too broad + logger.error("Failed") + +# Returning exceptions in JSON responses +except ValueError as e: + return JSONResponse( + content={"success": False, "message": str(e)}, + ) +``` + +### ✅ Use These Instead + +```python +# Async operations +import aiohttp +async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + data = await response.json() + +await asyncio.sleep(5) # ✅ Non-blocking + +# Translatable strings in React +const { t } = useTranslation(); +
    {t("camera_not_found")}
    # ✅ Translatable + +# Proper error handling +try: + data = await api.get_data() +except ApiException as err: + logger.error("API error: %s", err) + raise + +# Specific exceptions +try: + value = await sensor.read() +except SensorException as err: # ✅ Specific + logger.exception("Failed to read sensor") + +# Safe error responses +except ValueError: + logger.exception("Invalid parameters for API request") + return JSONResponse( + content={ + "success": False, + "message": "Invalid request parameters", + }, + ) +``` + +## WebSocket Broadcasts + +Outbound WebSocket broadcasts go through a per-recipient classifier in `frigate/comms/ws.py` that enforces camera-level access. **The classifier is fail-closed: any topic it doesn't recognize is dropped for every client.** New outbound topics must be classified there or they'll silently disappear. + +## Project-Specific Conventions + +### Configuration Files + +- Main config: `config/config.yml` + +### Directory Structure + +- Backend code: `frigate/` +- Frontend code: `web/` +- Docker files: `docker/` +- Documentation: `docs/` +- Database migrations: `migrations/` + +### Code Style Conformance + +Always conform new and refactored code to the existing coding style in the project: + +- Follow established patterns in similar files +- Match indentation and formatting of surrounding code +- Use consistent naming conventions (snake_case for Python, camelCase for TypeScript) +- Maintain the same level of verbosity in comments and docstrings + +## Additional Resources + +- Documentation: https://docs.frigate.video +- Main Repository: https://github.com/blakeblackshear/frigate +- Home Assistant Integration: https://github.com/blakeblackshear/frigate-hass-integration diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index d954bdcd52..d0b18ff805 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -252,6 +252,7 @@ http { include proxy.conf; proxy_cache api_cache; + proxy_cache_key "$scheme$proxy_host$request_uri|$role|$groups|$user"; proxy_cache_lock on; proxy_cache_use_stale updating; proxy_cache_valid 200 5s; diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 9c4e44051a..605eff92c3 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -2058,6 +2058,47 @@ paths: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" + /genai/models: + get: + tags: + - App + summary: List available GenAI models + description: Returns available models for each configured GenAI provider. + operationId: genai_models_genai_models_get + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + /genai/probe: + post: + tags: + - App + summary: Probe a GenAI provider without saving config + description: >- + Builds a transient client from the request body and returns its + available models. Used to validate provider credentials in the UI + before saving the configuration. Requires admin role. + operationId: genai_probe_genai_probe_post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GenAIProbeBody" + responses: + "200": + description: Successful Response + content: + application/json: + schema: {} + "422": + description: Validation Error + content: + application/json: + schema: + $ref: "#/components/schemas/HTTPValidationError" /vainfo: get: tags: @@ -7031,6 +7072,39 @@ components: "john_doe": ["face1.webp", "face2.jpg"], "jane_smith": ["face3.png"] } + GenAIProbeBody: + properties: + provider: + type: string + enum: + - openai + - azure_openai + - gemini + - ollama + - llamacpp + title: Provider + description: GenAI provider to probe + api_key: + anyOf: + - type: string + - type: "null" + title: API Key + description: API key for the provider (when applicable) + base_url: + anyOf: + - type: string + - type: "null" + title: Base URL + description: Base URL for self-hosted or compatible providers + provider_options: + type: object + title: Provider Options + description: Additional provider-specific options + default: {} + type: object + required: + - provider + title: GenAIProbeBody GenerateObjectExamplesBody: properties: model_name: diff --git a/frigate/api/app.py b/frigate/api/app.py index 4fac58a715..2ba016d63e 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -34,15 +34,17 @@ from frigate.api.auth import ( from frigate.api.defs.query.app_query_parameters import AppTimelineHourlyQueryParameters from frigate.api.defs.request.app_body import ( AppConfigSetBody, + GenAIProbeBody, MediaSyncBody, ) from frigate.api.defs.tags import Tags -from frigate.config import FrigateConfig +from frigate.config import FrigateConfig, GenAIConfig, GenAIProviderEnum from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateTopic, ) from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector +from frigate.genai import PROVIDERS, load_providers from frigate.jobs.media_sync import ( get_current_media_sync_job, get_media_sync_job_by_id, @@ -75,6 +77,14 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.app]) +# Short timeout for the /genai/probe path. The probe is interactive — fail +# fast on hung providers rather than holding an API worker thread. +_PROBE_TIMEOUT_SECONDS = 10 +# Outer cap that returns control to the caller even if the underlying sync +# HTTP call ignores its timeout. The sync work continues in the background +# thread; only the response is bounded. +_PROBE_OUTER_TIMEOUT_SECONDS = 15 + @router.get( "/", response_class=PlainTextResponse, dependencies=[Depends(allow_public())] @@ -170,6 +180,95 @@ def genai_models(request: Request): return JSONResponse(content=request.app.genai_manager.list_models()) +@router.post( + "/genai/probe", + dependencies=[Depends(require_role(["admin"]))], + summary="Probe a GenAI provider without saving config", + description=( + "Builds a transient client from the request body and returns its " + "available models. Used to validate provider credentials in the UI " + "before saving the configuration." + ), +) +async def genai_probe(body: GenAIProbeBody): + load_providers() + + provider_cls = PROVIDERS.get(body.provider) + if not provider_cls: + return JSONResponse( + status_code=400, + content={"success": False, "message": "Unknown provider"}, + ) + + # The OpenAI-compatible SDKs accept "timeout" as a constructor kwarg via + # provider_options; other plugins use GenAIClient.timeout passed below. + # Don't inject timeout for Gemini — its HttpOptions interprets the value + # in milliseconds and would clash with the plugin's own default. + probe_provider_options: dict[str, Any] = dict(body.provider_options or {}) + if body.provider in (GenAIProviderEnum.openai, GenAIProviderEnum.azure_openai): + probe_provider_options.setdefault("timeout", _PROBE_TIMEOUT_SECONDS) + + try: + transient_cfg = GenAIConfig( + provider=body.provider, + api_key=body.api_key, + base_url=body.base_url, + provider_options=probe_provider_options, + # model is required by the schema but irrelevant for listing. + model="probe", + roles=[], + ) + except ValidationError: + logger.exception("GenAI probe: invalid configuration") + return JSONResponse( + status_code=400, + content={"success": False, "message": "Invalid provider configuration"}, + ) + + try: + client = provider_cls( + transient_cfg, + timeout=_PROBE_TIMEOUT_SECONDS, + validate_model=False, + ) + except Exception: + logger.exception("GenAI probe: failed to construct client") + return JSONResponse( + content={ + "success": False, + "message": "Failed to connect to provider", + }, + ) + + try: + models = await asyncio.wait_for( + asyncio.to_thread(client.list_models), + timeout=_PROBE_OUTER_TIMEOUT_SECONDS, + ) + except asyncio.TimeoutError: + return JSONResponse( + content={"success": False, "message": "Probe timed out"}, + ) + except Exception: + logger.exception("GenAI probe: list_models failed") + return JSONResponse( + content={"success": False, "message": "Provider returned no models"}, + ) + + if not models: + return JSONResponse( + content={ + "success": False, + "message": ( + "No models returned. Check the API key, base URL, and " + "that the provider is reachable." + ), + }, + ) + + return JSONResponse(content={"success": True, "models": models}) + + @router.get("/config", dependencies=[Depends(allow_any_authenticated())]) def config(request: Request): config_obj: FrigateConfig = request.app.frigate_config diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index d9d11fd019..2c37f6ae4d 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -2,6 +2,8 @@ from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field +from frigate.config import GenAIProviderEnum + class AppConfigSetBody(BaseModel): requires_restart: int = 1 @@ -10,6 +12,13 @@ class AppConfigSetBody(BaseModel): skip_save: bool = False +class GenAIProbeBody(BaseModel): + provider: GenAIProviderEnum + api_key: Optional[str] = None + base_url: Optional[str] = None + provider_options: Dict[str, Any] = Field(default_factory=dict) + + class AppPutPasswordBody(BaseModel): password: str old_password: Optional[str] = None diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py index 902c94c42b..5b94755723 100644 --- a/frigate/config/camera/genai.py +++ b/frigate/config/camera/genai.py @@ -37,7 +37,7 @@ class GenAIConfig(FrigateBaseModel): description="Base URL for self-hosted or compatible providers (for example an Ollama instance).", ) model: str = Field( - default="gpt-4o", + default="", title="Model", description="The model to use from the provider for generating descriptions or summaries.", ) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 864092df58..28a6844d95 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -50,9 +50,15 @@ def register_genai_provider(key: GenAIProviderEnum) -> Callable: class GenAIClient: """Generative AI client for Frigate.""" - def __init__(self, genai_config: GenAIConfig, timeout: int = 120) -> None: + def __init__( + self, + genai_config: GenAIConfig, + timeout: int = 120, + validate_model: bool = True, + ) -> None: self.genai_config: GenAIConfig = genai_config self.timeout = timeout + self.validate_model = validate_model self.provider = self._init_provider() def generate_review_description( diff --git a/frigate/genai/plugins/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py index 830dd6817b..2dddf5244e 100644 --- a/frigate/genai/plugins/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -150,6 +150,10 @@ class LlamaCppClient(GenAIClient): else: base_url = base_url.replace("/v1", "") # Strip /v1 if included in base_url + if not self.validate_model: + # Probe path + return base_url + configured_model = self.genai_config.model info = self._get_model_info(base_url, configured_model) diff --git a/frigate/genai/plugins/ollama.py b/frigate/genai/plugins/ollama.py index a6f6d8ddd5..0f95dd3f9d 100644 --- a/frigate/genai/plugins/ollama.py +++ b/frigate/genai/plugins/ollama.py @@ -118,6 +118,9 @@ class OllamaClient(GenAIClient): timeout=self.timeout, headers=self._auth_headers(), ) + if not self.validate_model: + # Probe path + return client # ensure the model is available locally response = client.show(self.genai_config.model) if response.get("error"): diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index 2be0e65da8..3198515267 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -1,5 +1,8 @@ -from unittest.mock import Mock +from unittest.mock import Mock, patch +import frigate.genai +from frigate.config import GenAIProviderEnum +from frigate.genai import GenAIClient from frigate.models import Event, Recordings, ReviewSegment from frigate.stats.emitter import StatsEmitter from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp @@ -71,3 +74,94 @@ class TestHttpApp(BaseTestHttp): assert response.status_code == 200 assert app.frigate_config.cameras["front_door"].objects.track == ["person"] + + #################################################################################################################### + ################################### POST /genai/probe Endpoint ################################################## + #################################################################################################################### + def test_genai_probe_requires_admin(self): + app = super().create_app() + + with AuthTestClient(app) as client: + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + headers={"remote-user": "viewer", "remote-role": "viewer"}, + ) + assert response.status_code == 403 + + def test_genai_probe_returns_models_from_transient_client(self): + class FakeClient(GenAIClient): + def list_models(self): + return ["fake-model-a", "fake-model-b"] + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: FakeClient}, + ), + ): + response = client.post( + "/genai/probe", + json={ + "provider": "openai", + "api_key": "sk-test", + "base_url": "https://example.invalid", + }, + ) + assert response.status_code == 200 + assert response.json() == { + "success": True, + "models": ["fake-model-a", "fake-model-b"], + } + + def test_genai_probe_empty_list_is_treated_as_failure(self): + # The plugin's list_models() returns [] on connection failure rather + # than raising. The endpoint should surface that as success=false so + # the UI can show a meaningful error. + class EmptyClient(GenAIClient): + def list_models(self): + return [] + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: EmptyClient}, + ), + ): + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is False + assert "message" in payload + + def test_genai_probe_handles_provider_failure(self): + class FailingClient(GenAIClient): + def list_models(self): + raise RuntimeError("provider unreachable") + + app = super().create_app() + + with ( + AuthTestClient(app) as client, + patch.dict( + frigate.genai.PROVIDERS, + {GenAIProviderEnum.openai: FailingClient}, + ), + ): + response = client.post( + "/genai/probe", + json={"provider": "openai"}, + ) + assert response.status_code == 200 + payload = response.json() + assert payload["success"] is False + assert "message" in payload diff --git a/web/e2e/specs/replay.spec.ts b/web/e2e/specs/replay.spec.ts index c09abf10b2..51a42737f0 100644 --- a/web/e2e/specs/replay.spec.ts +++ b/web/e2e/specs/replay.spec.ts @@ -129,8 +129,14 @@ test.describe("Replay — active session @medium", () => { ); await actionGroup.first().click(); - const dialog = frigateApp.page.getByRole("dialog"); - await expect(dialog).toBeVisible({ timeout: 5_000 }); + // On mobile PlatformAwareSheet renders a MobilePage (full-screen panel) + // instead of a Radix Dialog, so assert the panel title heading is visible. + await expect( + frigateApp.page.getByRole("heading", { + level: 2, + name: /^Configuration$/i, + }), + ).toBeVisible({ timeout: 5_000 }); }); test("Objects tab renders with the camera_activity objects list", async ({ diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 9f842b79c0..65a3430269 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1557,9 +1557,14 @@ "searchPlaceholder": "Search...", "addCustomLabel": "Add custom label...", "genaiModel": { - "placeholder": "Select model…", - "search": "Search models…", - "noModels": "No models available" + "placeholder": "Select or enter a model…", + "search": "Search or enter a model…", + "noModels": "No models available", + "available": "Available models", + "useCustom": "Use \"{{value}}\"", + "refresh": "Refresh models", + "probeFailed": "Failed to probe models", + "fetchedModels": "Successfully fetched model list" } }, "globalConfig": { diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index e61ac8a6a7..b4b566fc51 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -1288,11 +1288,6 @@ export function ConfigSection({
    - {isOpen ? ( - - ) : ( - - )} {title} {showOverrideIndicator && effectiveLevel === "camera" && @@ -1323,12 +1318,17 @@ export function ConfigSection({ })} )} + {isOpen ? ( + + ) : ( + + )}
    -
    {sectionContent}
    +
    {sectionContent}
    diff --git a/web/src/components/config-form/sections/section-special-cases.ts b/web/src/components/config-form/sections/section-special-cases.ts index 256c275ebb..84bdaae9a9 100644 --- a/web/src/components/config-form/sections/section-special-cases.ts +++ b/web/src/components/config-form/sections/section-special-cases.ts @@ -171,7 +171,20 @@ function modifyObjectsSchema( ctx.fullConfig.objects?.track ?? []; - if (track.length === 0) return schema; + // Also promote any label that has a saved filter entry but isn't in + // `track` (e.g. the user toggled an object off but left a customized + // filter in YAML). Without this, RJSF falls back to the additional- + // properties Key/Value editor for those orphans. + const filtersSaved = + (ctx.level !== "global" + ? ctx.fullCameraConfig?.objects?.filters + : undefined) ?? + ctx.fullConfig.objects?.filters ?? + {}; + + if (track.length === 0 && Object.keys(filtersSaved).length === 0) { + return schema; + } const schemaProperties = isJsonObject( (schema as { properties?: unknown }).properties, @@ -199,16 +212,27 @@ function modifyObjectsSchema( ? (filtersSchema as { properties: Record }).properties : {}; - // Promote every tracked label to an explicit property entry so RJSF - // renders it as a normal collapsible (no additionalProperties key/value - // editor UI). Attribute labels get a restricted shape with only - // `min_score`; non-attribute labels get the full FilterConfig. Sorted - // alphabetically so the filter collapsibles match the order of the - // sibling `track` switches. - const sortedTrackedLabels = track - .filter((label): label is string => typeof label === "string") - .slice() - .sort((a, b) => a.localeCompare(b)); + // Promote every tracked label (and any orphaned filter entry) to an + // explicit property entry so RJSF renders it as a normal collapsible + // (no additionalProperties key/value editor UI). Attribute labels get a + // restricted shape with only `min_score`/`min_area`/`max_area`; + // non-attribute labels get the full FilterConfig. Sorted alphabetically + // so the filter collapsibles match the order of the sibling `track` + // switches. + const labelsToPromote = new Set(); + for (const label of track) { + if (typeof label === "string") labelsToPromote.add(label); + } + for (const key of Object.keys(filtersSaved)) { + // Skip attribute labels that aren't tracked — those are hidden + // entirely via hideAttributeFilters; promoting them would surface a + // collapsible we then have to hide separately. + if (attributeSet.has(key) && !labelsToPromote.has(key)) continue; + labelsToPromote.add(key); + } + const sortedTrackedLabels = [...labelsToPromote].sort((a, b) => + a.localeCompare(b), + ); const updatedFilterProperties: Record = { ...existingProperties, }; diff --git a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx index 3be8c0fe3b..294d061166 100644 --- a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx +++ b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx @@ -4,8 +4,11 @@ import { useState, useMemo, useEffect, useRef } from "react"; import type { WidgetProps } from "@rjsf/utils"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; -import { Check, ChevronsUpDown } from "lucide-react"; +import axios from "axios"; +import { Check, ChevronsUpDown, Plus, RefreshCw } from "lucide-react"; +import { LuCheck } from "react-icons/lu"; import { cn } from "@/lib/utils"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; import { Button } from "@/components/ui/button"; import { Command, @@ -19,9 +22,17 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import type { ConfigFormContext } from "@/types/configForm"; +import type { ConfigFormContext, JsonObject } from "@/types/configForm"; import { getSizedFieldClassName } from "../utils"; +type ProbeResponse = + | { success: true; models: string[] } + | { success: false; message: string }; + +type ProbeStatus = "idle" | "probing" | "success" | "error"; + +const PROBE_SUCCESS_INDICATOR_MS = 3000; + /** * Extract the provider config entry name from the RJSF widget id. * Widget ids look like "root_myProvider_model". @@ -41,6 +52,7 @@ export function GenAIModelWidget(props: WidgetProps) { const { id, value, disabled, readonly, onChange, options, registry } = props; const { t } = useTranslation(["views/settings"]); const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); const fieldClassName = getSizedFieldClassName(options, "sm"); const providerKey = useMemo(() => getProviderKey(id), [id]); @@ -77,78 +89,261 @@ export function GenAIModelWidget(props: WidgetProps) { } }, [configFingerprint, mutateModels]); - const models = useMemo(() => { + const fetchedModels = useMemo(() => { if (!allModels || !providerKey) return []; return allModels[providerKey] ?? []; }, [allModels, providerKey]); + const [probeStatus, setProbeStatus] = useState("idle"); + const [probeError, setProbeError] = useState(null); + const [probedModels, setProbedModels] = useState(null); + const probeSuccessTimerRef = useRef | null>( + null, + ); + + const probing = probeStatus === "probing"; + + // Reset probe results if the provider entry name changes + useEffect(() => { + setProbedModels(null); + setProbeError(null); + setProbeStatus("idle"); + if (probeSuccessTimerRef.current) { + clearTimeout(probeSuccessTimerRef.current); + probeSuccessTimerRef.current = null; + } + }, [providerKey]); + + useEffect(() => { + return () => { + if (probeSuccessTimerRef.current) { + clearTimeout(probeSuccessTimerRef.current); + } + }; + }, []); + + const models = probedModels ?? fetchedModels; + + const trimmedSearch = searchValue.trim(); + const matchesFetched = useMemo( + () => models.some((m) => m.toLowerCase() === trimmedSearch.toLowerCase()), + [models, trimmedSearch], + ); + const showCustomOption = trimmedSearch.length > 0 && !matchesFetched; + + // Read the live form values for this provider so probe sends the user's + // in-flight edits, not the saved config (which may not exist yet). + const formEntry = useMemo(() => { + if (!providerKey) return null; + const formData = formContext?.formData as JsonObject | undefined; + const entry = formData?.[providerKey]; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return null; + } + return entry as JsonObject; + }, [providerKey, formContext?.formData]); + + const formProvider = + typeof formEntry?.provider === "string" ? formEntry.provider : null; + const canProbe = Boolean(formProvider) && !probing; + + const probe = async () => { + if (!formEntry || !formProvider) return; + if (probeSuccessTimerRef.current) { + clearTimeout(probeSuccessTimerRef.current); + probeSuccessTimerRef.current = null; + } + setProbeStatus("probing"); + setProbeError(null); + try { + const res = await axios.post("genai/probe", { + provider: formProvider, + api_key: + typeof formEntry.api_key === "string" ? formEntry.api_key : null, + base_url: + typeof formEntry.base_url === "string" ? formEntry.base_url : null, + provider_options: + formEntry.provider_options && + typeof formEntry.provider_options === "object" && + !Array.isArray(formEntry.provider_options) + ? (formEntry.provider_options as JsonObject) + : {}, + }); + if (res.data.success) { + setProbedModels(res.data.models); + setProbeStatus("success"); + probeSuccessTimerRef.current = setTimeout(() => { + setProbeStatus("idle"); + probeSuccessTimerRef.current = null; + }, PROBE_SUCCESS_INDICATOR_MS); + } else { + setProbedModels([]); + setProbeError(res.data.message); + setProbeStatus("error"); + } + } catch { + setProbedModels(null); + setProbeError( + t("configForm.genaiModel.probeFailed", { + ns: "views/settings", + defaultValue: "Failed to probe models", + }), + ); + setProbeStatus("error"); + } + }; + + const commit = (next: string) => { + onChange(next); + setSearchValue(""); + setOpen(false); + }; + const currentLabel = typeof value === "string" && value ? value : undefined; + const refreshLabel = t("configForm.genaiModel.refresh", { + ns: "views/settings", + defaultValue: "Refresh models", + }); + return ( - - - - - - - - - {models.length > 0 ? ( - - {models.map((model) => ( - { - onChange(model); - setOpen(false); - }} - > - - {model} - - ))} - - ) : ( -
    - {t("configForm.genaiModel.noModels", { + +
    - )} -
    -
    -
    -
    + + + + + + { + if (e.key === "Enter" && showCustomOption) { + e.preventDefault(); + commit(trimmedSearch); + } + }} + /> + + {showCustomOption && ( + + commit(trimmedSearch)} + > + + {t("configForm.genaiModel.useCustom", { + ns: "views/settings", + value: trimmedSearch, + defaultValue: 'Use "{{value}}"', + })} + + + )} + {models.length > 0 ? ( + + {models.map((model) => ( + commit(model)} + > + + {model} + + ))} + + ) : !showCustomOption ? ( +
    + {t("configForm.genaiModel.noModels", { + ns: "views/settings", + defaultValue: "No models available", + })} +
    + ) : null} +
    +
    +
    + + + +
    + {probeStatus === "success" && ( + + + {t("configForm.genaiModel.fetchedModels", { + ns: "views/settings", + defaultValue: "Successfully fetched model list", + })} + + )} + {probeStatus === "error" && probeError && ( + {probeError} + )} +
    + ); } diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index cd73f2ac3d..2cf8debfee 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -27,13 +27,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/ui/dialog"; +import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog"; import { useCameraActivity } from "@/hooks/use-camera-activity"; import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; @@ -333,15 +327,64 @@ export default function Replay() { )}
    - + + + + {t("page.configuration")} + + + } + title={t("page.configuration")} + titleClassName="text-lg font-semibold" + contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl" + content={ + <> +

    + {t("page.configurationDesc")} +

    + {configSchema == null ? ( +
    + +
    + ) : ( +
    + + +
    + )} + + } + open={configDialogOpen} + onOpenChange={setConfigDialogOpen} + /> @@ -644,49 +687,6 @@ export default function Replay() {
    - - - - - {t("page.configuration")} - - {t("page.configurationDesc")} - - - {configSchema == null ? ( -
    - -
    - ) : ( -
    - - -
    - )} -
    -
    ); } diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index c83dbcc1c9..a34e4b7e2c 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -162,7 +162,6 @@ const allSettingsViews = [ "cameraLpr", "cameraMqttConfig", "cameraOnvif", - "cameraUi", "cameraTimestampStyle", "cameraManagement", "masksAndZones", @@ -292,9 +291,6 @@ const CameraMqttConfigSettingsPage = createSectionPage("mqtt", "camera", { const CameraOnvifSettingsPage = createSectionPage("onvif", "camera", { showOverrideIndicator: false, }); -const CameraUiSettingsPage = createSectionPage("ui", "camera", { - showOverrideIndicator: false, -}); const CameraTimestampStyleSettingsPage = createSectionPage( "timestamp_style", "camera", @@ -361,7 +357,6 @@ const settingsGroups = [ { key: "cameraLpr", component: CameraLprSettingsPage }, { key: "cameraOnvif", component: CameraOnvifSettingsPage }, { key: "cameraMqttConfig", component: CameraMqttConfigSettingsPage }, - { key: "cameraUi", component: CameraUiSettingsPage }, { key: "cameraTimestampStyle", component: CameraTimestampStyleSettingsPage, @@ -467,7 +462,6 @@ const CAMERA_SELECT_BUTTON_PAGES = [ "cameraLpr", "cameraMqttConfig", "cameraOnvif", - "cameraUi", "cameraTimestampStyle", "masksAndZones", "motionTuner", @@ -495,7 +489,6 @@ const CAMERA_SECTION_MAPPING: Record = { lpr: "cameraLpr", mqtt: "cameraMqttConfig", onvif: "cameraOnvif", - ui: "cameraUi", timestamp_style: "cameraTimestampStyle", }; From a576ad52185217be2526f9c6934ff23cdab95e65 Mon Sep 17 00:00:00 2001 From: Sean Kelly Date: Wed, 20 May 2026 09:52:47 -0700 Subject: [PATCH 29/94] Refactor move_preview_frames function (#23264) Refactor move_preview_frames to simplify logic and improve error handling. --- frigate/output/output.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/frigate/output/output.py b/frigate/output/output.py index 79ef349c8f..67dba5221f 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -342,20 +342,30 @@ def move_preview_frames(loc: str) -> None: preview_holdover = os.path.join(CLIPS_DIR, "preview_restart_cache") preview_cache = os.path.join(CACHE_DIR, "preview_frames") + if loc == "clips": + src = preview_cache + dst = preview_holdover + elif loc == "cache": + src = preview_holdover + dst = preview_cache + else: + return + try: - if loc == "clips": - shutil.move(preview_cache, preview_holdover) - elif loc == "cache": - if not os.path.exists(preview_holdover): - return + if not os.path.exists(src): + return - if not os.access(preview_holdover, os.R_OK | os.W_OK): - logger.error( - "Insufficient permissions on preview restart cache at %s", - preview_holdover, - ) - return + shutil.move(src, dst) - shutil.move(preview_holdover, preview_cache) + except PermissionError: + logger.error( + "Insufficient permissions while moving preview restart cache from %s to %s", + src, + dst, + ) except shutil.Error: - logger.error("Failed to restore preview cache.") + logger.error( + "Failed to move preview restart cache from %s to %s", + src, + dst, + ) From 5ef8b9b92466a0ad3107b2da14e481618a5dd194 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 May 2026 16:37:02 -0500 Subject: [PATCH 30/94] Debug replay fixes (#23270) * ensure motion masks from source camera are copied to replay * stop polling debug_replay/status after live_ready * use vod for constructing replay clips --- frigate/api/debug_replay.py | 5 +++ frigate/debug_replay.py | 14 ++++++++- frigate/jobs/debug_replay.py | 39 +++++++++++++---------- frigate/test/test_debug_replay_job.py | 45 +++++++++++++++++++++------ web/src/pages/Replay.tsx | 2 +- 5 files changed, 77 insertions(+), 28 deletions(-) diff --git a/frigate/api/debug_replay.py b/frigate/api/debug_replay.py index 2ba5d2b85f..034da3845d 100644 --- a/frigate/api/debug_replay.py +++ b/frigate/api/debug_replay.py @@ -86,10 +86,15 @@ class DebugReplayStopResponse(BaseModel): async def start_debug_replay(request: Request, body: DebugReplayStartBody): """Start a debug replay session asynchronously.""" replay_manager = request.app.replay_manager + internal_port = request.app.frigate_config.networking.listen.internal + if type(internal_port) is str: + internal_port = int(internal_port.split(":")[-1]) + source = RecordingDebugReplaySource( source_camera=body.camera, start_ts=body.start_time, end_ts=body.end_time, + internal_port=internal_port, ) try: diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index a0c122134c..557e6447fd 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -245,11 +245,23 @@ class DebugReplayManager: "frame_shape", "raw_mask", "mask", - "improved_contrast_enabled", + "enabled_in_config", "rasterized_mask", } ) + if source_config.motion.mask: + motion_dict["mask"] = { + mask_id: ( + mask_cfg.model_dump( + exclude={"raw_coordinates", "enabled_in_config"} + ) + if mask_cfg is not None + else None + ) + for mask_id, mask_cfg in source_config.motion.mask.items() + } + return { "enabled": True, "ffmpeg": { diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py index 3dd7a02bf6..3d8b2d6b63 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -1,4 +1,4 @@ -"""Debug replay startup job: ffmpeg concat + camera config publish. +"""Debug replay startup job: ffmpeg remux + camera config publish. The runner orchestrates the async portion of starting a debug replay session. The DebugReplayManager (in frigate.debug_replay) owns session @@ -153,15 +153,22 @@ class DebugReplaySource(ABC): class RecordingDebugReplaySource(DebugReplaySource): """Replay source backed by the Recordings table. - Builds a concat playlist of recording files covering the time range - and feeds it to ffmpeg's concat demuxer. + Feeds ffmpeg the internal VOD endpoint so segments with mismatched + SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS + discontinuities. """ - def __init__(self, source_camera: str, start_ts: float, end_ts: float) -> None: + def __init__( + self, + source_camera: str, + start_ts: float, + end_ts: float, + internal_port: int, + ) -> None: self._camera = source_camera self._start_ts = start_ts self._end_ts = end_ts - self._concat_file: Optional[str] = None + self._internal_port = internal_port @property def source_camera(self) -> str: @@ -185,18 +192,16 @@ class RecordingDebugReplaySource(DebugReplaySource): ) def ffmpeg_input_args(self, working_dir: str) -> list[str]: - replay_name = f"{REPLAY_CAMERA_PREFIX}{self._camera}" - concat_file = os.path.join(working_dir, f"{replay_name}_concat.txt") - recordings = query_recordings(self._camera, self._start_ts, self._end_ts) - with open(concat_file, "w") as f: - for recording in recordings: - f.write(f"file '{recording.path}'\n") - self._concat_file = concat_file - return ["-f", "concat", "-safe", "0", "-i", concat_file] - - def cleanup(self, working_dir: str) -> None: - if self._concat_file: - _remove_silent(self._concat_file) + playlist_url = ( + f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}" + f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8" + ) + return [ + "-protocol_whitelist", + "pipe,file,http,tcp", + "-i", + playlist_url, + ] class ExportDebugReplaySource(DebugReplaySource): diff --git a/frigate/test/test_debug_replay_job.py b/frigate/test/test_debug_replay_job.py index 5e2da16720..12c4be82b8 100644 --- a/frigate/test/test_debug_replay_job.py +++ b/frigate/test/test_debug_replay_job.py @@ -101,7 +101,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="missing", start_ts=100.0, end_ts=200.0 + source_camera="missing", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -112,7 +115,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=200.0, end_ts=100.0 + source_camera="front", + start_ts=200.0, + end_ts=100.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -126,7 +132,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(ValueError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -156,7 +165,10 @@ class TestStartDebugReplayJob(unittest.TestCase): ): job_id = start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -193,7 +205,10 @@ class TestStartDebugReplayJob(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -203,7 +218,10 @@ class TestStartDebugReplayJob(unittest.TestCase): with self.assertRaises(RuntimeError): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -271,7 +289,10 @@ class TestRunnerHappyPath(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -342,7 +363,10 @@ class TestRunnerFailurePath(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, @@ -420,7 +444,10 @@ class TestRunnerCancellation(unittest.TestCase): ): start_debug_replay_job( source=RecordingDebugReplaySource( - source_camera="front", start_ts=100.0, end_ts=200.0 + source_camera="front", + start_ts=100.0, + end_ts=200.0, + internal_port=5000, ), frigate_config=self.frigate_config, config_publisher=self.publisher, diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index 2cf8debfee..05c618509c 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -121,7 +121,7 @@ export default function Replay() { mutate: refreshStatus, isLoading, } = useSWR("debug_replay/status", { - refreshInterval: 1000, + refreshInterval: (latestData) => (latestData?.live_ready ? 0 : 1000), }); const { payload: replayJob } = useJobStatus("debug_replay"); From 68e8afd35c76f05f68de47ee9588d2c91796de4b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 May 2026 16:59:01 -0500 Subject: [PATCH 31/94] Improve credential redaction handling (#23265) * redact credentials in config endpoint with sentinel * backend test * frontend * apply widget for credential fields * i18n --- frigate/api/app.py | 38 +++++++++++++------ frigate/const.py | 2 + frigate/test/http_api/test_http_app.py | 15 ++++++++ frigate/util/config.py | 17 ++++++++- web/public/locales/en/common.json | 5 ++- .../config-form/section-configs/genai.ts | 1 + .../config-form/section-configs/mqtt.ts | 1 + .../config-form/section-configs/onvif.ts | 3 ++ .../config-form/section-configs/proxy.ts | 1 + .../theme/widgets/PasswordWidget.tsx | 22 +++++++++-- web/src/lib/const.ts | 10 +++++ web/src/utils/configUtil.ts | 28 ++++++++++++++ 12 files changed, 126 insertions(+), 17 deletions(-) diff --git a/frigate/api/app.py b/frigate/api/app.py index 2ba016d63e..cc5adc6f65 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -43,6 +43,7 @@ from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateTopic, ) +from frigate.const import REDACTED_CREDENTIAL_SENTINEL from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector from frigate.genai import PROVIDERS, load_providers from frigate.jobs.media_sync import ( @@ -61,7 +62,11 @@ from frigate.util.builtin import ( process_config_query_string, update_yaml_file_bulk, ) -from frigate.util.config import apply_section_update, find_config_file +from frigate.util.config import ( + apply_section_update, + find_config_file, + redact_credential, +) from frigate.util.schema import get_config_schema from frigate.util.services import ( get_nvidia_driver_info, @@ -284,26 +289,24 @@ def config(request: Request): if request.headers.get("remote-role") != "admin": config.pop("environment_vars", None) - # remove mqtt credentials - config["mqtt"].pop("password", None) - config["mqtt"].pop("user", None) + # redact mqtt credentials + redact_credential(config["mqtt"], "password") - # remove the proxy secret - config["proxy"].pop("auth_secret", None) + # redact proxy secret + redact_credential(config["proxy"], "auth_secret") - # remove genai api keys - for genai_name, genai_cfg in config.get("genai", {}).items(): + # redact genai api keys + for _genai_name, genai_cfg in config.get("genai", {}).items(): if isinstance(genai_cfg, dict): - genai_cfg.pop("api_key", None) + redact_credential(genai_cfg, "api_key") for camera_name, camera in request.app.frigate_config.cameras.items(): camera_dict = config["cameras"][camera_name] - # remove onvif credentials + # redact onvif credentials onvif_dict = camera_dict.get("onvif", {}) if onvif_dict: - onvif_dict.pop("user", None) - onvif_dict.pop("password", None) + redact_credential(onvif_dict, "password") # clean paths for input in camera_dict.get("ffmpeg", {}).get("inputs", []): @@ -680,6 +683,10 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo _restore_masked_camera_paths(body.config_data, request.app.frigate_config) updates = flatten_config_data(body.config_data) updates = {k: ("" if v is None else v) for k, v in updates.items()} + # Drop any field whose value is still the redaction sentinel + updates = { + k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL + } if not updates: return JSONResponse( @@ -790,6 +797,13 @@ def config_set(request: Request, body: AppConfigSetBody): updates = flatten_config_data(body.config_data) # Convert None values to empty strings for deletion (e.g., when deleting masks) updates = {k: ("" if v is None else v) for k, v in updates.items()} + # Drop sentinel-valued fields so untouched credential + # placeholders don't clobber the saved YAML value. + updates = { + k: v + for k, v in updates.items() + if v != REDACTED_CREDENTIAL_SENTINEL + } if not updates: return JSONResponse( diff --git a/frigate/const.py b/frigate/const.py index 07537ea5f7..dac04c4f1a 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -21,6 +21,8 @@ PLUS_API_HOST = "https://api.frigate.video" SHM_FRAMES_VAR = "SHM_MAX_FRAMES" +REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__" + # Attribute & Object constants DEFAULT_ATTRIBUTE_LABEL_MAP = { diff --git a/frigate/test/http_api/test_http_app.py b/frigate/test/http_api/test_http_app.py index 3198515267..4c581dd426 100644 --- a/frigate/test/http_api/test_http_app.py +++ b/frigate/test/http_api/test_http_app.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch import frigate.genai from frigate.config import GenAIProviderEnum +from frigate.const import REDACTED_CREDENTIAL_SENTINEL from frigate.genai import GenAIClient from frigate.models import Event, Recordings, ReviewSegment from frigate.stats.emitter import StatsEmitter @@ -75,6 +76,20 @@ class TestHttpApp(BaseTestHttp): assert response.status_code == 200 assert app.frigate_config.cameras["front_door"].objects.track == ["person"] + #################################################################################################################### + ################################### Credential redaction sentinel ################################################ + #################################################################################################################### + def test_config_response_redacts_mqtt_password_with_sentinel(self): + self.minimal_config["mqtt"]["user"] = "mqttuser" + self.minimal_config["mqtt"]["password"] = "supersecret" + app = super().create_app() + + with AuthTestClient(app) as client: + response = client.get("/config") + assert response.status_code == 200 + mqtt = response.json()["mqtt"] + assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL + #################################################################################################################### ################################### POST /genai/probe Endpoint ################################################## #################################################################################################################### diff --git a/frigate/util/config.py b/frigate/util/config.py index 71e2af809d..e6e3f09666 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -8,7 +8,7 @@ from typing import Any, Optional, Union from ruamel.yaml import YAML -from frigate.const import CONFIG_DIR, EXPORT_DIR +from frigate.const import CONFIG_DIR, EXPORT_DIR, REDACTED_CREDENTIAL_SENTINEL from frigate.util.builtin import deep_merge from frigate.util.services import get_video_properties @@ -18,6 +18,21 @@ CURRENT_CONFIG_VERSION = "0.18-0" DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, "config.yml") +def redact_credential(obj: dict[str, Any], key: str) -> None: + """Replace obj[key] with the redaction sentinel if a value is saved, else drop. + + Used when shaping the /config response so saved credentials never leave + the server. The frontend recognizes REDACTED_CREDENTIAL_SENTINEL, renders + the field as empty with a "saved — leave blank to keep" placeholder, and + /config/set strips it from any incoming payload so the YAML value is + preserved when the user doesn't touch the field. + """ + if obj.get(key): + obj[key] = REDACTED_CREDENTIAL_SENTINEL + else: + obj.pop(key, None) + + def find_config_file() -> str: config_path = os.environ.get("CONFIG_FILE", DEFAULT_CONFIG_FILE) diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index a05126c681..4436808d08 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -316,5 +316,8 @@ "pixels": "{{area}}px" }, "no_items": "No items", - "validation_errors": "Validation Errors" + "validation_errors": "Validation Errors", + "credentialField": { + "savedPlaceholder": "Saved — leave blank to keep current" + } } diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts index a5f1cd8a32..bd7a875c8e 100644 --- a/web/src/components/config-form/section-configs/genai.ts +++ b/web/src/components/config-form/section-configs/genai.ts @@ -24,6 +24,7 @@ const genai: SectionConfigOverrides = { "ui:widget": "genaiRoles", }, "*.api_key": { + "ui:widget": "password", "ui:options": { size: "lg" }, }, "*.base_url": { diff --git a/web/src/components/config-form/section-configs/mqtt.ts b/web/src/components/config-form/section-configs/mqtt.ts index 67d863b089..52fd6b6d79 100644 --- a/web/src/components/config-form/section-configs/mqtt.ts +++ b/web/src/components/config-form/section-configs/mqtt.ts @@ -64,6 +64,7 @@ const mqtt: SectionConfigOverrides = { liveValidate: true, uiSchema: { password: { + "ui:widget": "password", "ui:options": { size: "xs" }, }, }, diff --git a/web/src/components/config-form/section-configs/onvif.ts b/web/src/components/config-form/section-configs/onvif.ts index 71163a0341..edb62ff7fe 100644 --- a/web/src/components/config-form/section-configs/onvif.ts +++ b/web/src/components/config-form/section-configs/onvif.ts @@ -29,6 +29,9 @@ const onvif: SectionConfigOverrides = { host: { "ui:options": { size: "sm" }, }, + password: { + "ui:widget": "password", + }, profile: { "ui:widget": "onvifProfile", }, diff --git a/web/src/components/config-form/section-configs/proxy.ts b/web/src/components/config-form/section-configs/proxy.ts index ffdb27cf9f..08e05b6b86 100644 --- a/web/src/components/config-form/section-configs/proxy.ts +++ b/web/src/components/config-form/section-configs/proxy.ts @@ -18,6 +18,7 @@ const proxy: SectionConfigOverrides = { "ui:options": { size: "lg" }, }, auth_secret: { + "ui:widget": "password", "ui:options": { size: "md" }, }, header_map: { diff --git a/web/src/components/config-form/theme/widgets/PasswordWidget.tsx b/web/src/components/config-form/theme/widgets/PasswordWidget.tsx index 80a4e504ee..a7a09af7b3 100644 --- a/web/src/components/config-form/theme/widgets/PasswordWidget.tsx +++ b/web/src/components/config-form/theme/widgets/PasswordWidget.tsx @@ -3,8 +3,10 @@ import type { WidgetProps } from "@rjsf/utils"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { LuEye, LuEyeOff } from "react-icons/lu"; import { cn } from "@/lib/utils"; +import { REDACTED_CREDENTIAL_SENTINEL } from "@/lib/const"; import { getSizedFieldClassName } from "../utils"; export function PasswordWidget(props: WidgetProps) { @@ -21,17 +23,31 @@ export function PasswordWidget(props: WidgetProps) { options, } = props; + const { t } = useTranslation(["common"]); const [showPassword, setShowPassword] = useState(false); const fieldClassName = getSizedFieldClassName(options, "sm"); + // When the backend returns the sentinel, hide it visually and prompt the + // user that a value is already saved. The value stays as the sentinel in + // form state — backend /config/set strips it so the saved YAML is + // preserved when the user doesn't touch the field. + const isRedacted = value === REDACTED_CREDENTIAL_SENTINEL; + const displayValue = isRedacted ? "" : (value ?? ""); + const effectivePlaceholder = isRedacted + ? t("credentialField.savedPlaceholder", { + ns: "common", + defaultValue: "Saved — leave blank to keep current", + }) + : placeholder || ""; + return (
    onChange(e.target.value === "" ? undefined : e.target.value) } @@ -46,7 +62,7 @@ export function PasswordWidget(props: WidgetProps) { size="sm" className="absolute right-0 top-0 h-full px-3 hover:bg-transparent" onClick={() => setShowPassword(!showPassword)} - disabled={disabled} + disabled={disabled || isRedacted} > {showPassword ? ( diff --git a/web/src/lib/const.ts b/web/src/lib/const.ts index 96aa1f4b18..5db13e375d 100644 --- a/web/src/lib/const.ts +++ b/web/src/lib/const.ts @@ -1,6 +1,16 @@ /** ONNX embedding models that require local model downloads. GenAI providers are not in this list. */ export const JINA_EMBEDDING_MODELS = ["jinav1", "jinav2"] as const; +/** + * Sentinel the backend substitutes for saved credentials (api keys, + * passwords, secrets) in /config responses. The credential widget renders + * this value as an empty input with a "saved — leave blank to keep" hint, + * and stripRedactedCredentials() removes any field still equal to this + * value before sending a config/set payload so the saved YAML value is + * preserved. Mirror of frigate.const.REDACTED_CREDENTIAL_SENTINEL. + */ +export const REDACTED_CREDENTIAL_SENTINEL = "__FRIGATE_SAVED_CREDENTIAL__"; + export const ANNOTATION_OFFSET_MIN = -10000; export const ANNOTATION_OFFSET_MAX = 5000; export const ANNOTATION_OFFSET_STEP = 50; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index 4b6ffefb71..cb7f6f52b6 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -11,6 +11,7 @@ import isEqual from "lodash/isEqual"; import mergeWith from "lodash/mergeWith"; import set from "lodash/set"; import { isJsonObject } from "@/lib/utils"; +import { REDACTED_CREDENTIAL_SENTINEL } from "@/lib/const"; import { applySchemaDefaults } from "@/lib/config-schema"; import { normalizeConfigValue } from "@/hooks/use-config-override"; import { @@ -29,6 +30,33 @@ import type { import type { SectionConfig } from "../components/config-form/sections/BaseSection"; import { sectionConfigs } from "../components/config-form/sectionConfigs"; +/** + * Recursively strip any key whose value is the redaction sentinel from a + * config_data payload. Use just before sending to /config/set so untouched + * credential placeholder fields don't clobber the saved YAML value. Mutates + * and returns the input. + */ +export function stripRedactedCredentials(value: T): T { + if (Array.isArray(value)) { + for (const item of value) { + stripRedactedCredentials(item); + } + return value; + } + if (value && typeof value === "object") { + const obj = value as Record; + for (const key of Object.keys(obj)) { + const v = obj[key]; + if (v === REDACTED_CREDENTIAL_SENTINEL) { + delete obj[key]; + } else if (v && typeof v === "object") { + stripRedactedCredentials(v); + } + } + } + return value; +} + // --------------------------------------------------------------------------- // cameraUpdateTopicMap — maps config section paths to MQTT/WS update topics // --------------------------------------------------------------------------- From 01c82d6921987c743c2ae1280eebcd70b7474bdb Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 20 May 2026 19:38:00 -0600 Subject: [PATCH 32/94] Improve language around prompt restrictions (#23274) --- frigate/genai/prompts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py index 9ff81cdebd..93e19209bf 100644 --- a/frigate/genai/prompts.py +++ b/frigate/genai/prompts.py @@ -63,8 +63,8 @@ Describe the scene based on observable actions and movements, evaluate the activ ## Analysis Guidelines When forming your description: -- **CRITICAL: Only describe objects explicitly listed in "Objects in Scene" below.** Do not infer or mention additional people, vehicles, or objects not present in this list, even if visual patterns suggest them. If only a car is listed, do not describe a person interacting with it unless "person" is also in the objects list. -- **Only describe actions actually visible in the frames.** Do not assume or infer actions that you don't observe happening. If someone walks toward furniture but you never see them sit, do not say they sat. Stick to what you can see across the sequence. +- **Treat "Objects in Scene" as the list of tracked subjects to describe.** Do not introduce additional people or vehicles that are not present in this list. You may freely reference other items, surfaces, and environmental details visible in the frames when describing what the listed subjects are doing. +- **Describe the most likely activity from visible cues across the sequence** — the subject's path, what they are carrying, and what they interact with. Avoid asserting completed outcomes you do not observe; describe in-progress actions rather than results. - Describe what you observe: actions, movements, interactions with objects and the environment. Include any observable environmental changes (e.g., lighting changes triggered by activity). - Note visible details such as clothing, items being carried or placed, tools or equipment present, and how they interact with the property or objects. - Consider the full sequence chronologically: what happens from start to finish, how duration and actions relate to the location and objects involved. From 555ef89800dd36bf6e7f46778a2a3b6523a36ff1 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 21 May 2026 09:12:53 -0500 Subject: [PATCH 33/94] Debug replay fixes (#23276) * filter replay camera from camera selectors * add face rec and lpr to replay configuration sheet * add missing config topic subscriptions in embeddings maintainer * pop replay camera from config object when stopping --- frigate/debug_replay.py | 1 + frigate/embeddings/maintainer.py | 7 +++++ .../classification/wizard/Step2StateArea.tsx | 2 ++ .../NotificationsSettingsExtras.tsx | 5 +++- .../components/overlay/CreateRoleDialog.tsx | 5 +++- .../overlay/EditRoleCamerasDialog.tsx | 5 +++- web/src/components/overlay/ExportDialog.tsx | 5 +++- .../overlay/detail/ObjectPathPlotter.tsx | 23 +++++++++------ web/src/hooks/use-config-override.ts | 15 +++++++--- web/src/hooks/use-has-full-camera-access.ts | 3 +- web/src/pages/Events.tsx | 2 +- web/src/pages/Replay.tsx | 28 +++++++++++++++++++ web/src/pages/Settings.tsx | 8 +++++- web/src/views/events/EventView.tsx | 9 ++++-- .../views/settings/CameraManagementView.tsx | 16 +++++++++-- .../settings/FrigatePlusSettingsView.tsx | 9 +++--- web/src/views/settings/ProfilesView.tsx | 5 +++- web/src/views/system/CameraMetrics.tsx | 3 +- 18 files changed, 119 insertions(+), 32 deletions(-) diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index 557e6447fd..b137e24b93 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -169,6 +169,7 @@ class DebugReplayManager: CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name), frigate_config.cameras[replay_name], ) + frigate_config.cameras.pop(replay_name, None) if replay_name is not None: self._cleanup_db(replay_name) diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 48c5cdd79b..52bdf5d915 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -98,10 +98,17 @@ class EmbeddingMaintainer(threading.Thread): [ CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.detect, + CameraConfigUpdateEnum.face_recognition, + CameraConfigUpdateEnum.ffmpeg, + CameraConfigUpdateEnum.lpr, + CameraConfigUpdateEnum.motion, + CameraConfigUpdateEnum.objects, CameraConfigUpdateEnum.object_genai, CameraConfigUpdateEnum.review, CameraConfigUpdateEnum.review_genai, CameraConfigUpdateEnum.semantic_search, + CameraConfigUpdateEnum.zones, ], ) self.enrichment_config_subscriber = ConfigSubscriber("config/") diff --git a/web/src/components/classification/wizard/Step2StateArea.tsx b/web/src/components/classification/wizard/Step2StateArea.tsx index efba0d358a..84367a877d 100644 --- a/web/src/components/classification/wizard/Step2StateArea.tsx +++ b/web/src/components/classification/wizard/Step2StateArea.tsx @@ -14,6 +14,7 @@ import Konva from "konva"; import { useResizeObserver } from "@/hooks/resize-observer"; import { useApiHost } from "@/api"; import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; +import { isReplayCamera } from "@/utils/cameraUtil"; import Heading from "@/components/ui/heading"; import { isMobile } from "react-device-detect"; import { cn } from "@/lib/utils"; @@ -67,6 +68,7 @@ export default function Step2StateArea({ ([name, cam]) => cam.enabled && cam.enabled_in_config && + !isReplayCamera(name) && !selectedCameraNames.includes(name), ) .map(([name]) => ({ diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index b97c90448d..fb53055bcc 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -57,6 +57,7 @@ import isEqual from "lodash/isEqual"; import set from "lodash/set"; import type { ConfigSectionData, JsonObject } from "@/types/configForm"; import { sanitizeSectionData } from "@/utils/configUtil"; +import { isReplayCamera } from "@/utils/cameraUtil"; import type { SectionRendererProps } from "./registry"; const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js"; @@ -94,7 +95,7 @@ export default function NotificationsSettingsExtras({ return Object.values(config.cameras) .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order) - .filter((c) => c.enabled_in_config); + .filter((c) => c.enabled_in_config && !isReplayCamera(c.name)); }, [config]); const notificationCameras = useMemo(() => { @@ -106,6 +107,7 @@ export default function NotificationsSettingsExtras({ .filter( (conf) => conf.enabled_in_config && + !isReplayCamera(conf.name) && conf.notifications && conf.notifications.enabled_in_config, ) @@ -359,6 +361,7 @@ export default function NotificationsSettingsExtras({ Object.values(config.cameras).some( (c) => c.enabled_in_config && + !isReplayCamera(c.name) && c.notifications && c.notifications.enabled_in_config, ), diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx index 023d2b6650..2a1b79b761 100644 --- a/web/src/components/overlay/CreateRoleDialog.tsx +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -26,6 +26,7 @@ import { import { useTranslation } from "react-i18next"; import { FrigateConfig } from "@/types/frigateConfig"; import { CameraNameLabel } from "../camera/FriendlyNameLabel"; +import { isReplayCamera } from "@/utils/cameraUtil"; import { isDesktop, isMobile } from "react-device-detect"; import { cn } from "@/lib/utils"; import { @@ -52,7 +53,9 @@ export default function CreateRoleDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}); + const cameras = Object.keys(config.cameras || {}).filter( + (name) => !isReplayCamera(name), + ); const existingRoles = Object.keys(config.auth?.roles || {}); diff --git a/web/src/components/overlay/EditRoleCamerasDialog.tsx b/web/src/components/overlay/EditRoleCamerasDialog.tsx index f533fc5b85..f23dc0931a 100644 --- a/web/src/components/overlay/EditRoleCamerasDialog.tsx +++ b/web/src/components/overlay/EditRoleCamerasDialog.tsx @@ -25,6 +25,7 @@ import { import { Trans, useTranslation } from "react-i18next"; import { FrigateConfig } from "@/types/frigateConfig"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; +import { isReplayCamera } from "@/utils/cameraUtil"; type EditRoleCamerasOverlayProps = { show: boolean; @@ -46,7 +47,9 @@ export default function EditRoleCamerasDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}); + const cameras = Object.keys(config.cameras || {}).filter( + (name) => !isReplayCamera(name), + ); const formSchema = z.object({ cameras: z diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 0d57821fc1..6d407ecc34 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -54,6 +54,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Textarea } from "../ui/textarea"; import { useNavigate } from "react-router-dom"; import { useIsAdmin } from "@/hooks/use-is-admin"; +import { isReplayCamera } from "@/utils/cameraUtil"; const EXPORT_OPTIONS = [ "1", @@ -448,7 +449,9 @@ export function ExportContent({ ); const cameraActivities = useMemo(() => { - const allCameraIds = Object.keys(config?.cameras ?? {}); + const allCameraIds = Object.keys(config?.cameras ?? {}).filter( + (name) => !isReplayCamera(name), + ); const byCamera = new Map(); events?.forEach((event) => { diff --git a/web/src/components/overlay/detail/ObjectPathPlotter.tsx b/web/src/components/overlay/detail/ObjectPathPlotter.tsx index 1fcf02d1db..aa65ae4091 100644 --- a/web/src/components/overlay/detail/ObjectPathPlotter.tsx +++ b/web/src/components/overlay/detail/ObjectPathPlotter.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/select"; import { Card, CardContent } from "@/components/ui/card"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; +import { isReplayCamera } from "@/utils/cameraUtil"; import { useTimezone } from "@/hooks/use-date-utils"; import { Button } from "@/components/ui/button"; import { LuX } from "react-icons/lu"; @@ -36,11 +37,16 @@ export default function ObjectPathPlotter() { const [currentPage, setCurrentPage] = useState(1); const eventsPerPage = 20; + const cameraNames = useMemo(() => { + if (!config) return []; + return Object.keys(config.cameras).filter((name) => !isReplayCamera(name)); + }, [config]); + useEffect(() => { - if (config && !selectedCamera) { - setSelectedCamera(Object.keys(config.cameras)[0]); + if (cameraNames.length > 0 && !selectedCamera) { + setSelectedCamera(cameraNames[0]); } - }, [config, selectedCamera]); + }, [cameraNames, selectedCamera]); const searchQuery = useMemo(() => { if (!selectedCamera) return null; @@ -143,12 +149,11 @@ export default function ObjectPathPlotter() { - {config && - Object.keys(config.cameras).map((cameraName) => ( - - {cameraName} - - ))} + {cameraNames.map((cameraName) => ( + + {cameraName} + + ))} setInput(e.target.value)} + onKeyDown={handleKeyDown} + aria-busy={isLoading} + /> + {showStop ? ( + + ) : ( + + )} +
    + + ); +} diff --git a/web/src/components/chat/ChatStartingState.tsx b/web/src/components/chat/ChatStartingState.tsx index a0a3a044c8..3e77677379 100644 --- a/web/src/components/chat/ChatStartingState.tsx +++ b/web/src/components/chat/ChatStartingState.tsx @@ -1,15 +1,22 @@ import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { FaArrowUpLong } from "react-icons/fa6"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import type { StartingRequest } from "@/types/chat"; +import { ChatComposer } from "@/components/chat/ChatComposer"; type ChatStartingStateProps = { onSendMessage: (message: string) => void; + supportsThinking: boolean; + thinkingEnabled: boolean; + setThinkingEnabled: (value: boolean | undefined) => void; }; -export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) { +export function ChatStartingState({ + onSendMessage, + supportsThinking, + thinkingEnabled, + setThinkingEnabled, +}: ChatStartingStateProps) { const { t } = useTranslation(["views/chat"]); const [input, setInput] = useState(""); @@ -36,20 +43,13 @@ export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) { onSendMessage(prompt); }; - const handleSubmit = () => { - const text = input.trim(); + const handleSend = (textOverride?: string) => { + const text = (textOverride ?? input).trim(); if (!text) return; onSendMessage(text); setInput(""); }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }; - return (
    @@ -77,22 +77,17 @@ export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) {
    -
    - + setInput(e.target.value)} - onKeyDown={handleKeyDown} + supportsThinking={supportsThinking} + thinkingEnabled={thinkingEnabled} + setThinkingEnabled={setThinkingEnabled} + large /> -
    ); diff --git a/web/src/components/chat/ReasoningBubble.tsx b/web/src/components/chat/ReasoningBubble.tsx index dd7c8fe819..07dc7f5bec 100644 --- a/web/src/components/chat/ReasoningBubble.tsx +++ b/web/src/components/chat/ReasoningBubble.tsx @@ -8,6 +8,12 @@ import { } from "@/components/ui/collapsible"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; type ReasoningBubbleProps = { /** The accumulated reasoning text from the model. */ @@ -54,34 +60,42 @@ export function ReasoningBubble({ return (
    - - - - - -
    -            {reasoning}
    -          
    -
    -
    + + + + + + +
    +              {reasoning}
    +            
    +
    +
    +
    ); } diff --git a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx index 294d061166..ca5b30d29f 100644 --- a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx +++ b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx @@ -23,6 +23,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import type { ConfigFormContext, JsonObject } from "@/types/configForm"; +import type { GenAIModelsResponse } from "@/types/chat"; import { getSizedFieldClassName } from "../utils"; type ProbeResponse = @@ -73,11 +74,12 @@ export function GenAIModelWidget(props: WidgetProps) { return `${e.provider ?? ""}|${e.base_url ?? ""}`; }, [providerKey, formContext?.fullConfig]); - const { data: allModels, mutate: mutateModels } = useSWR< - Record - >("genai/models", { - revalidateOnFocus: false, - }); + const { data: allModels, mutate: mutateModels } = useSWR( + "genai/models", + { + revalidateOnFocus: false, + }, + ); // Revalidate models when the saved config fingerprint changes (e.g. after // switching provider or base_url and saving). @@ -89,9 +91,9 @@ export function GenAIModelWidget(props: WidgetProps) { } }, [configFingerprint, mutateModels]); - const fetchedModels = useMemo(() => { + const fetchedModels = useMemo(() => { if (!allModels || !providerKey) return []; - return allModels[providerKey] ?? []; + return allModels[providerKey]?.models ?? []; }, [allModels, providerKey]); const [probeStatus, setProbeStatus] = useState("idle"); diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index 4621c97540..7103a189d1 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -1,20 +1,21 @@ import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { FaArrowUpLong, FaStop } from "react-icons/fa6"; import { LuCircleAlert, LuMessageSquarePlus } from "react-icons/lu"; import { useTranslation } from "react-i18next"; import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import axios from "axios"; +import useSWR from "swr"; import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { MessageBubble } from "@/components/chat/ChatMessage"; import { ReasoningBubble } from "@/components/chat/ReasoningBubble"; import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup"; import { ChatStartingState } from "@/components/chat/ChatStartingState"; -import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip"; -import { ChatQuickReplies } from "@/components/chat/ChatQuickReplies"; -import { ChatPaperclipButton } from "@/components/chat/ChatPaperclipButton"; +import { ChatComposer } from "@/components/chat/ChatComposer"; import ChatSettings from "@/components/chat/ChatSettings"; -import type { ChatMessage, ShowStatsMode } from "@/types/chat"; +import type { + ChatMessage, + GenAIModelsResponse, + ShowStatsMode, +} from "@/types/chat"; import { usePersistence } from "@/hooks/use-persistence"; import { getEventIdsFromSearchObjectsToolCalls, @@ -38,9 +39,26 @@ export default function ChatPage() { "chat-auto-scroll", true, ); + const [thinkingEnabled, setThinkingEnabled] = usePersistence( + "chat-thinking-enabled", + false, + ); const scrollRef = useRef(null); const abortRef = useRef(null); + const { data: genaiInfo } = useSWR("genai/models", { + revalidateOnFocus: false, + }); + const supportsThinking = useMemo(() => { + if (!genaiInfo) return false; + for (const entry of Object.values(genaiInfo)) { + if (entry.roles?.includes("chat") && entry.supports_toggleable_thinking) { + return true; + } + } + return false; + }, [genaiInfo]); + useEffect(() => { document.title = t("documentTitle"); }, [t]); @@ -100,9 +118,10 @@ export default function ChatPage() { defaultErrorMessage: t("error"), }, controller.signal, + supportsThinking ? { enableThinking: !!thinkingEnabled } : {}, ); }, - [isLoading, t], + [isLoading, supportsThinking, t, thinkingEnabled], ); const recentEventIds = useMemo(() => { @@ -305,6 +324,9 @@ export default function ChatPage() { setInput(""); submitConversation([{ role: "user", content: message }]); }} + supportsThinking={supportsThinking} + thinkingEnabled={!!thinkingEnabled} + setThinkingEnabled={setThinkingEnabled} /> )} @@ -313,7 +335,7 @@ export default function ChatPage() { {hasStarted && (
    -
    @@ -331,89 +356,3 @@ export default function ChatPage() { ); } - -type ChatEntryProps = { - input: string; - setInput: (value: string) => void; - sendMessage: (textOverride?: string) => void; - isLoading: boolean; - placeholder: string; - attachedEventId: string | null; - onClearAttachment: () => void; - onAttach: (eventId: string) => void; - onStop: () => void; - recentEventIds: string[]; -}; - -function ChatEntry({ - input, - setInput, - sendMessage, - isLoading, - placeholder, - attachedEventId, - onClearAttachment, - onAttach, - onStop, - recentEventIds, -}: ChatEntryProps) { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - sendMessage(); - } - }; - - return ( -
    - {attachedEventId && ( -
    - -
    - )} - {attachedEventId && ( - sendMessage(text)} - disabled={isLoading} - /> - )} -
    - - setInput(e.target.value)} - onKeyDown={handleKeyDown} - aria-busy={isLoading} - /> - {isLoading ? ( - - ) : ( - - )} -
    -
    - ); -} diff --git a/web/src/types/chat.ts b/web/src/types/chat.ts index db6d84bf58..81c16820ff 100644 --- a/web/src/types/chat.ts +++ b/web/src/types/chat.ts @@ -25,3 +25,11 @@ export type ChatStats = { }; export type ShowStatsMode = "while_generating" | "always"; + +export type GenAIProviderInfo = { + models: string[]; + roles: string[]; + supports_toggleable_thinking: boolean; +}; + +export type GenAIModelsResponse = Record; diff --git a/web/src/utils/chatUtil.ts b/web/src/utils/chatUtil.ts index 5389f7aff8..73e5c213b6 100644 --- a/web/src/utils/chatUtil.ts +++ b/web/src/utils/chatUtil.ts @@ -34,12 +34,17 @@ type StreamChunk = * POST to chat/completion with stream: true, parse NDJSON stream, and invoke * callbacks so the caller can update UI (e.g. React state). */ +export type StreamChatOptions = { + enableThinking?: boolean; +}; + export async function streamChatCompletion( url: string, headers: Record, apiMessages: { role: string; content: string }[], callbacks: StreamChatCallbacks, signal?: AbortSignal, + options: StreamChatOptions = {}, ): Promise { const { updateMessages, @@ -50,10 +55,17 @@ export async function streamChatCompletion( } = callbacks; try { + const body: Record = { + messages: apiMessages, + stream: true, + }; + if (options.enableThinking !== undefined) { + body.enable_thinking = options.enableThinking; + } const res = await fetch(url, { method: "POST", headers, - body: JSON.stringify({ messages: apiMessages, stream: true }), + body: JSON.stringify(body), signal, }); From a4a592b4e633d526513909e46854d31deb846655 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 21 May 2026 14:38:38 -0600 Subject: [PATCH 35/94] Cleanup and fix mypy (#23283) --- frigate/genai/__init__.py | 37 ++++++++++++++++++++++++++++++++- frigate/genai/plugins/gemini.py | 3 +++ frigate/genai/plugins/openai.py | 4 ++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index cf51550b47..bca5e6d691 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -5,7 +5,7 @@ import json import logging import os import re -from typing import Any, Callable, Optional +from typing import Any, AsyncGenerator, Callable, Optional import numpy as np from pydantic import ValidationError @@ -359,6 +359,41 @@ class GenAIClient: "finish_reason": "error", } + async def chat_with_tools_stream( + self, + messages: list[dict[str, Any]], + tools: Optional[list[dict[str, Any]]] = None, + tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, + ) -> AsyncGenerator[tuple[str, Any], None]: + """Streaming counterpart to `chat_with_tools`. + + Yields ``(kind, value)`` tuples where ``kind`` is one of: + - 'content_delta': value is a string fragment of the answer + - 'reasoning_delta': value is a string fragment of the reasoning + trace (emitted before content for thinking models) + - 'stats': value is a usage stats dict + - 'message': value is the final dict shape described in + `chat_with_tools` + + Argument semantics — including ``enable_thinking`` — match + `chat_with_tools`. Providers that don't support streaming should + override this and yield an error 'message' event. + """ + logger.warning( + f"{self.__class__.__name__} does not support chat_with_tools_stream. " + "This method should be overridden by the provider implementation." + ) + yield ( + "message", + { + "content": None, + "reasoning": None, + "tool_calls": None, + "finish_reason": "error", + }, + ) + def load_providers() -> None: plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") diff --git a/frigate/genai/plugins/gemini.py b/frigate/genai/plugins/gemini.py index 6e4b9283fb..8c05e0b1ad 100644 --- a/frigate/genai/plugins/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -369,11 +369,14 @@ class GeminiClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. Implements streaming function calling/tool usage for Gemini models. + ``enable_thinking`` is accepted for interface parity; Gemini configures + thinking at the model level, so it is ignored here. """ try: # Convert messages to Gemini format diff --git a/frigate/genai/plugins/openai.py b/frigate/genai/plugins/openai.py index 8d422bfb31..3e862f8fd5 100644 --- a/frigate/genai/plugins/openai.py +++ b/frigate/genai/plugins/openai.py @@ -309,11 +309,15 @@ class OpenAIClient(GenAIClient): messages: list[dict[str, Any]], tools: Optional[list[dict[str, Any]]] = None, tool_choice: Optional[str] = "auto", + enable_thinking: Optional[bool] = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. Implements streaming function calling/tool usage for OpenAI models. + The OpenAI chat completions API does not expose a per-request thinking + toggle, so ``enable_thinking`` is accepted for interface parity and + ignored. """ try: openai_tool_choice = None From 0bdf5002a0d5f5380381b9a662f14fcc6f346678 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 22 May 2026 08:52:01 -0500 Subject: [PATCH 36/94] Miscellaneous fixes (#23279) * use monotonic clock for detector inference duration to prevent negative values from wall clock steps * add ability to set camera's webui_url from camera management pane * Gemini send thought signature * Update docs * copy face and lpr configs from source camera to replay camera * add guard * improve dummy camera docs * remove version number * fix stale field message after reverting a conditional form field Routes field-level conditional messages through a dedicated React Context instead of merging them into uiSchema. RJSF's Form keeps state.uiSchema sticky across renders during processPendingChange (formData is updated, uiSchema is not), so a previously injected ui:messages array stays attached to a field even after the triggering condition flips back to false. Context propagation re-runs FieldTemplate directly on every provider value change, sidestepping that staleness. * add semantic search field message to note that model_size is irrelevant when embeddings provider is selected --------- Co-authored-by: Nicolas Mowen --- docs/docs/configuration/genai/config.md | 11 +- docs/docs/configuration/review.md | 2 +- docs/docs/troubleshooting/dummy-camera.md | 1 + frigate/debug_replay.py | 8 +- frigate/genai/plugins/gemini.py | 65 ++++- frigate/genai/utils.py | 8 + frigate/object_detection/base.py | 3 +- frigate/ptz/autotrack.py | 2 + web/public/locales/en/views/settings.json | 17 +- .../config-form/FieldMessagesContext.ts | 13 + .../section-configs/semantic_search.ts | 16 ++ .../config-form/sections/BaseSection.tsx | 147 +++++------- .../theme/templates/FieldTemplate.tsx | 24 +- .../views/settings/CameraManagementView.tsx | 225 +++++++++++++++--- 14 files changed, 384 insertions(+), 158 deletions(-) create mode 100644 web/src/components/config-form/FieldMessagesContext.ts diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md index a512943c90..9f396d3ccc 100644 --- a/docs/docs/configuration/genai/config.md +++ b/docs/docs/configuration/genai/config.md @@ -49,15 +49,14 @@ You should have at least 8 GB of RAM available (or VRAM if running on GPU) to ru ### Model Types: Instruct vs Thinking -Most vision-language models are available as **instruct** models, which are fine-tuned to follow instructions and respond concisely to prompts. However, some models (such as certain Qwen-VL or minigpt variants) offer both **instruct** and **thinking** versions. +Vision-language models come in **instruct** variants (fine-tuned to follow instructions and respond concisely), **thinking** variants (fine-tuned for free-form, speculative reasoning), and **hybrid** variants that support both modes per request. Most modern vision-language models are hybrid. -- **Instruct models** are always recommended for use with Frigate. These models generate direct, relevant, actionable descriptions that best fit Frigate's object and event summary use case. -- **Reasoning / Thinking models** are fine-tuned for more free-form, open-ended, and speculative outputs, which are typically not concise and may not provide the practical summaries Frigate expects. For this reason, Frigate does **not** recommend or support using thinking models. +Frigate manages reasoning per task automatically: -Some models are labeled as **hybrid** (capable of both thinking and instruct tasks). In these cases, it is recommended to disable reasoning / thinking, which is generally model specific (see your models documentation). +- **Description tasks** (object descriptions, review descriptions, review summaries) are synthesis-only and benefit from concise, direct output, so Frigate disables thinking for these calls when the model exposes a per-request toggle. +- **Chat** lets you toggle thinking on or off from the composer when the configured model supports it. -**Recommendation:** -Always select the `-instruct` or documented instruct/tagged variant of any model you use in your Frigate configuration. If in doubt, refer to your model provider's documentation or model library for guidance on the correct model variant to use. +You can use a pure instruct, hybrid, or thinking-capable model with Frigate — no extra configuration is required to disable thinking for descriptions. ### llama.cpp diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md index 4f39611dbe..be02bdd8e9 100644 --- a/docs/docs/configuration/review.md +++ b/docs/docs/configuration/review.md @@ -23,7 +23,7 @@ In 0.14 and later, all of that is bundled into a single review item which starts ## Alerts and Detections -Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate 0.14 categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring required zones for them. +Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring required zones for them. :::note diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md index aed0af5e68..7e9831e4be 100644 --- a/docs/docs/troubleshooting/dummy-camera.md +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -56,6 +56,7 @@ Only one replay session can be active at a time. If a session is already running - The replay will not always produce identical results to the original run. Different frames may be selected on replay, which can change detections and tracking. - Motion detection depends on the exact frames used; small frame shifts can change motion regions and therefore what gets passed to the detector. - Object detection is not fully deterministic: models and post-processing can yield slightly different results across runs. +- In cases where a detection is short and a replay may only be a small number of frames, it is recommended to manually add some padding before and after the detection so that the motion and object detectors have time to settle into the scene. Rather than starting Debug Replay from Explore, navigate to History for your camera, choose Debug Replay from the Actions menu, and click the "From Timeline" or "Custom" option. Treat the replay as a close approximation rather than an exact reproduction. Run multiple loops and examine the debug overlays and logs to understand the behavior. diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index b137e24b93..ea95e153c1 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -238,6 +238,10 @@ class DebugReplayManager: zone_dump.setdefault("coordinates", zone_config.coordinates) zones_dict[zone_name] = zone_dump + # Extract LPR and face recognition configs + lpr_dict = source_config.lpr.model_dump() + face_recognition_dict = source_config.face_recognition.model_dump() + # Extract motion config (exclude runtime fields) motion_dict = {} if source_config.motion is not None: @@ -287,8 +291,8 @@ class DebugReplayManager: }, "birdseye": {"enabled": False}, "audio": {"enabled": False}, - "lpr": {"enabled": False}, - "face_recognition": {"enabled": False}, + "lpr": lpr_dict, + "face_recognition": face_recognition_dict, } def _cleanup_db(self, camera_name: str) -> None: diff --git a/frigate/genai/plugins/gemini.py b/frigate/genai/plugins/gemini.py index 8c05e0b1ad..9efd241893 100644 --- a/frigate/genai/plugins/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -1,5 +1,7 @@ """Gemini Provider for Frigate AI.""" +import base64 +import binascii import json import logging from typing import Any, AsyncGenerator, Optional @@ -14,6 +16,27 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) +def _decode_thought_signature(value: Any) -> Optional[bytes]: + """Decode a base64-encoded thought_signature carried across conversation turns.""" + if not value: + return None + if isinstance(value, bytes): + return value + if isinstance(value, str): + try: + return base64.b64decode(value) + except (binascii.Error, ValueError): + return None + return None + + +def _encode_thought_signature(signature: Optional[bytes]) -> Optional[str]: + """Encode bytes thought_signature as base64 so it survives JSON-friendly transport.""" + if not signature: + return None + return base64.b64encode(signature).decode("ascii") + + def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]: """Build a stats dict from a Gemini usage_metadata object.""" prompt_tokens = getattr(usage, "prompt_token_count", None) @@ -169,11 +192,17 @@ class GeminiClient(GenAIClient): if not isinstance(tc_args, dict): tc_args = {} if tc_name: - parts.append( - types.Part.from_function_call( - name=tc_name, args=tc_args - ) + fc_part = types.Part.from_function_call( + name=tc_name, args=tc_args ) + # Thinking-capable Gemini models require the original + # thought_signature to be echoed back on functionCall + # parts after a tool response, or the next request + # fails with INVALID_ARGUMENT. + sig = _decode_thought_signature(tc.get("thought_signature")) + if sig: + fc_part.thought_signature = sig + parts.append(fc_part) if not parts: parts.append(types.Part.from_text(text=" ")) gemini_messages.append(types.Content(role="model", parts=parts)) @@ -310,6 +339,9 @@ class GeminiClient(GenAIClient): "id": part.function_call.name or "", "name": part.function_call.name or "", "arguments": arguments, + "thought_signature": _encode_thought_signature( + getattr(part, "thought_signature", None) + ), } ) @@ -418,11 +450,17 @@ class GeminiClient(GenAIClient): if not isinstance(tc_args, dict): tc_args = {} if tc_name: - parts.append( - types.Part.from_function_call( - name=tc_name, args=tc_args - ) + fc_part = types.Part.from_function_call( + name=tc_name, args=tc_args ) + # Thinking-capable Gemini models require the original + # thought_signature to be echoed back on functionCall + # parts after a tool response, or the next request + # fails with INVALID_ARGUMENT. + sig = _decode_thought_signature(tc.get("thought_signature")) + if sig: + fc_part.thought_signature = sig + parts.append(fc_part) if not parts: parts.append(types.Part.from_text(text=" ")) gemini_messages.append(types.Content(role="model", parts=parts)) @@ -588,6 +626,7 @@ class GeminiClient(GenAIClient): "id": tool_call_id, "name": tool_call_name, "arguments": "", + "thought_signature": None, } # Accumulate arguments @@ -598,6 +637,13 @@ class GeminiClient(GenAIClient): else str(arguments) ) + # Capture latest thought_signature for this call + chunk_sig = getattr(part, "thought_signature", None) + if chunk_sig: + tool_calls_by_index[found_index][ + "thought_signature" + ] = chunk_sig + # Build final message full_content = "".join(content_parts).strip() or None full_reasoning = "".join(reasoning_parts).strip() or None @@ -618,6 +664,9 @@ class GeminiClient(GenAIClient): "id": tc["id"], "name": tc["name"], "arguments": parsed_args, + "thought_signature": _encode_thought_signature( + tc.get("thought_signature") + ), } ) finish_reason = "tool_calls" diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py index 44f982059b..a382647cb9 100644 --- a/frigate/genai/utils.py +++ b/frigate/genai/utils.py @@ -69,6 +69,14 @@ def build_assistant_message_for_conversation( "name": tc["name"], "arguments": json.dumps(tc.get("arguments") or {}), }, + # Gemini-only: opaque signature that must be echoed back on + # the same functionCall part in the next turn. Other providers + # do not set or read this. + **( + {"thought_signature": tc["thought_signature"]} + if tc.get("thought_signature") + else {} + ), } for tc in tool_calls_raw ] diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index a62fe48431..f2336f3da8 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -167,8 +167,9 @@ class DetectorRunner(FrigateProcess): # detect and send the output self.start_time.value = datetime.datetime.now().timestamp() + mono_start = time.monotonic() detections = object_detector.detect_raw(input_frame) - duration = datetime.datetime.now().timestamp() - self.start_time.value + duration = time.monotonic() - mono_start frame_manager.close(connection_id) if connection_id not in self.outputs: diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 1a45f619c2..fb76f6718d 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -1331,6 +1331,8 @@ class PtzAutoTracker: return self.tracked_object[camera]["region"] def autotrack_object(self, camera: str, obj: TrackedObject): + if camera not in self.config.cameras: + return camera_config = self.config.cameras[camera] if camera_config.onvif.autotracking.enabled: diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 65a3430269..7bb582120b 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -484,11 +484,15 @@ "reorderHandle": "Drag to reorder", "saving": "Saving…", "saved": "Saved", - "friendlyName": { - "edit": "Edit camera display name", - "title": "Edit Display Name", - "description": "Set the friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", - "rename": "Rename" + "details": { + "edit": "Edit camera details", + "title": "Edit Camera Details", + "description": "Update the display name and external URL used for this camera throughout the Frigate UI.", + "friendlyNameLabel": "Display Name", + "friendlyNameHelp": "Friendly name shown for this camera throughout the Frigate UI. Leave blank to use the camera ID.", + "webuiUrlLabel": "Camera Web UI URL", + "webuiUrlHelp": "URL to visit the camera's web UI directly from the Debug view. Leave blank to disable the link.", + "webuiUrlInvalid": "Must be a valid URL (e.g., https://example.com)." } }, "cameraConfig": { @@ -1816,7 +1820,8 @@ "mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}." }, "semanticSearch": { - "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended." + "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended.", + "modelSizeIgnoredForProvider": "Model size only applies to the built-in Jina models. This value will be ignored when using a GenAI embedding provider." } } } diff --git a/web/src/components/config-form/FieldMessagesContext.ts b/web/src/components/config-form/FieldMessagesContext.ts new file mode 100644 index 0000000000..5d45f7d79b --- /dev/null +++ b/web/src/components/config-form/FieldMessagesContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import type { FieldConditionalMessage } from "./section-configs/types"; + +// Provides currently-active field messages to FieldTemplate without going +// through RJSF's per-field uiSchema. RJSF caches state.uiSchema across renders +// in a way that can leave stale ui:messages attached to a field when the +// triggering condition flips back to false (see processPendingChange in +// @rjsf/core Form.js — formData is updated immediately, uiSchema is not). +// useContext re-runs consumers directly on provider value change, sidestepping +// that staleness. +export const FieldMessagesContext = createContext( + [], +); diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts index 884401b7d2..3f9bbfaec1 100644 --- a/web/src/components/config-form/section-configs/semantic_search.ts +++ b/web/src/components/config-form/section-configs/semantic_search.ts @@ -29,6 +29,22 @@ const semanticSearch: SectionConfigOverrides = { ctx.formData?.model === "jinav2" && ctx.formData?.model_size === "small", }, + { + key: "model-size-ignored-for-provider", + field: "model_size", + messageKey: "configMessages.semanticSearch.modelSizeIgnoredForProvider", + severity: "info", + position: "after", + condition: (ctx) => { + const model = ctx.formData?.model; + return ( + typeof model === "string" && + model !== "" && + model !== "jinav1" && + model !== "jinav2" + ); + }, + }, ], uiSchema: { model: { diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index b4b566fc51..5bacd2d80c 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -86,6 +86,7 @@ import type { } from "../section-configs/types"; import { useConfigMessages } from "@/hooks/use-config-messages"; import { ConfigMessageBanner } from "../ConfigMessageBanner"; +import { FieldMessagesContext } from "../FieldMessagesContext"; export interface SectionConfig { /** Field ordering within the section */ @@ -627,44 +628,6 @@ export function ConfigSection({ messageContext, ); - // Merge field-level conditional messages into uiSchema - const effectiveUiSchema = useMemo(() => { - if (activeFieldMessages.length === 0) return sectionConfig.uiSchema; - const merged = { ...(sectionConfig.uiSchema ?? {}) }; - for (const msg of activeFieldMessages) { - const segments = msg.field.split("."); - // Navigate to the nested uiSchema node, shallow-cloning along the way - let node = merged; - for (let i = 0; i < segments.length - 1; i++) { - const seg = segments[i]; - node[seg] = { ...(node[seg] as Record) }; - node = node[seg] as Record; - } - const leafKey = segments[segments.length - 1]; - const existing = node[leafKey] as Record | undefined; - const existingMessages = ((existing?.["ui:messages"] as unknown[]) ?? - []) as Array<{ - key: string; - messageKey: string; - severity: string; - position?: string; - }>; - node[leafKey] = { - ...existing, - "ui:messages": [ - ...existingMessages, - { - key: msg.key, - messageKey: msg.messageKey, - severity: msg.severity, - position: msg.position ?? "before", - }, - ], - }; - } - return merged; - }, [sectionConfig.uiSchema, activeFieldMessages]); - const currentOverrides = useMemo(() => { if (!currentFormData || typeof currentFormData !== "object") { return undefined; @@ -1034,59 +997,61 @@ export function ConfigSection({ const sectionContent = (
    - handleChange(data), - // For widgets that need access to full camera config (e.g., zone names) - fullCameraConfig: - effectiveLevel === "camera" && cameraName - ? config?.cameras?.[cameraName] - : undefined, - fullConfig: config, - // When rendering camera-level sections, provide the section path so - // field templates can look up keys under the `config/cameras` namespace - // When using a consolidated global namespace, keys are nested - // under the section name (e.g., `audio.label`) so provide the - // section prefix to templates so they can attempt `${section}.${field}` lookups. - sectionI18nPrefix: sectionPath, - t, - renderers: wrappedRenderers, - sectionDocs: sectionConfig.sectionDocs, - fieldDocs: sectionConfig.fieldDocs, - hiddenFields: effectiveHiddenFields, - restartRequired: sectionConfig.restartRequired, - requiresRestart, - isProfile: !!profileName, - }} - /> + + handleChange(data), + // For widgets that need access to full camera config (e.g., zone names) + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config?.cameras?.[cameraName] + : undefined, + fullConfig: config, + // When rendering camera-level sections, provide the section path so + // field templates can look up keys under the `config/cameras` namespace + // When using a consolidated global namespace, keys are nested + // under the section name (e.g., `audio.label`) so provide the + // section prefix to templates so they can attempt `${section}.${field}` lookups. + sectionI18nPrefix: sectionPath, + t, + renderers: wrappedRenderers, + sectionDocs: sectionConfig.sectionDocs, + fieldDocs: sectionConfig.fieldDocs, + hiddenFields: effectiveHiddenFields, + restartRequired: sectionConfig.restartRequired, + requiresRestart, + isProfile: !!profileName, + }} + /> + {!embedded && (
    {children}
    ; @@ -384,21 +386,15 @@ export function FieldTemplate(props: FieldTemplateProps) { const beforeContent = renderCustom(beforeSpec); const afterContent = renderCustom(afterSpec); - // Render conditional field messages from ui:messages - const fieldMessageSpecs = uiSchema?.["ui:messages"] as - | Array<{ - key: string; - messageKey: string; - severity: string; - position?: string; - }> - | undefined; - const beforeMessages = fieldMessageSpecs?.filter( + // Read field-level conditional messages from FieldMessagesContext + const fieldPathStr = pathSegments.join("."); + const fieldMessageSpecs = allFieldMessages.filter( + (m) => m.field === fieldPathStr, + ); + const beforeMessages = fieldMessageSpecs.filter( (m) => (m.position ?? "before") === "before", ); - const afterMessages = fieldMessageSpecs?.filter( - (m) => m.position === "after", - ); + const afterMessages = fieldMessageSpecs.filter((m) => m.position === "after"); const beforeMessagesContent = beforeMessages && beforeMessages.length > 0 ? (
    diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index b43baf170f..212b32389a 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -36,7 +36,15 @@ import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import RestartRequiredIndicator from "@/components/indicators/RestartRequiredIndicator"; -import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, @@ -53,6 +61,17 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; const REORDER_SAVED_INDICATOR_MS = 1500; @@ -482,7 +501,7 @@ function EnabledCameraRow({ - @@ -519,25 +538,91 @@ function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) { ); } -type CameraFriendlyNameEditorProps = { +type CameraDetailsEditorProps = { cameraName: string; onConfigChanged: () => Promise; }; -function CameraFriendlyNameEditor({ +type CameraDetailsFormValues = { + friendlyName: string; + webuiUrl: string; +}; + +function CameraDetailsEditor({ cameraName, onConfigChanged, -}: CameraFriendlyNameEditorProps) { +}: CameraDetailsEditorProps) { const { t } = useTranslation(["views/settings", "common"]); const { data: config } = useSWR("config"); const [open, setOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); const currentFriendlyName = config?.cameras?.[cameraName]?.friendly_name; + const currentWebuiUrl = config?.cameras?.[cameraName]?.webui_url; - const onSave = useCallback( - async (text: string) => { + const formSchema = useMemo( + () => + z.object({ + friendlyName: z.string(), + webuiUrl: z.string().refine( + (val) => { + const trimmed = val.trim(); + if (!trimmed) return true; + try { + new URL(trimmed); + return true; + } catch { + return false; + } + }, + { + message: t("cameraManagement.streams.details.webuiUrlInvalid", { + ns: "views/settings", + }), + }, + ), + }), + [t], + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + friendlyName: currentFriendlyName ?? "", + webuiUrl: currentWebuiUrl ?? "", + }, + }); + + // Reset form values from config whenever the dialog is opened. + useEffect(() => { + if (open) { + form.reset({ + friendlyName: currentFriendlyName ?? "", + webuiUrl: currentWebuiUrl ?? "", + }); + } + }, [open, currentFriendlyName, currentWebuiUrl, form]); + + const onSubmit = useCallback( + async (values: CameraDetailsFormValues) => { if (isSaving) return; + + // only send fields the user actually changed + const newFriendly = values.friendlyName.trim() || null; + const newWebui = values.webuiUrl.trim() || null; + const cameraUpdate: Record = {}; + if (newFriendly !== (currentFriendlyName ?? null)) { + cameraUpdate.friendly_name = newFriendly; + } + if (newWebui !== (currentWebuiUrl ?? null)) { + cameraUpdate.webui_url = newWebui; + } + + if (Object.keys(cameraUpdate).length === 0) { + setOpen(false); + return; + } + setIsSaving(true); try { @@ -545,9 +630,7 @@ function CameraFriendlyNameEditor({ requires_restart: 0, config_data: { cameras: { - [cameraName]: { - friendly_name: text.trim() || null, - }, + [cameraName]: cameraUpdate, }, }, }); @@ -573,10 +656,17 @@ function CameraFriendlyNameEditor({ setIsSaving(false); } }, - [cameraName, isSaving, onConfigChanged, t], + [ + cameraName, + currentFriendlyName, + currentWebuiUrl, + isSaving, + onConfigChanged, + t, + ], ); - const renameLabel = t("cameraManagement.streams.friendlyName.rename", { + const editLabel = t("cameraManagement.streams.details.edit", { ns: "views/settings", }); @@ -588,30 +678,107 @@ function CameraFriendlyNameEditor({ variant="ghost" size="icon" className="size-7" - aria-label={renameLabel} + aria-label={editLabel} onClick={() => setOpen(true)} disabled={isSaving} > - {renameLabel} + {editLabel} - + + + + + {t("cameraManagement.streams.details.title", { + ns: "views/settings", + })} + + + {t("cameraManagement.streams.details.description", { + ns: "views/settings", + })} + + +
    + + ( + + + {t("cameraManagement.streams.details.friendlyNameLabel", { + ns: "views/settings", + })} + + + + +

    + {t("cameraManagement.streams.details.friendlyNameHelp", { + ns: "views/settings", + })} +

    + +
    + )} + /> + ( + + + {t("cameraManagement.streams.details.webuiUrlLabel", { + ns: "views/settings", + })} + + + + +

    + {t("cameraManagement.streams.details.webuiUrlHelp", { + ns: "views/settings", + })} +

    + +
    + )} + /> + + + + + + +
    +
    ); } From 3a09d01bbe110cff4f25ea36f73a203292c92e1d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 22 May 2026 09:39:52 -0500 Subject: [PATCH 37/94] Debug replay resolution (#23287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * unlink shm frames when camera is removed * drop stale shm cache refs when cached segment is too small for requested shape * skip new-object frame cache write when current_frame is unavailable * add tests * use setdefault when adding a new camera Multiple subscribers in the same process each unpickle the ZMQ payload independently and would otherwise write divergent Python objects to the shared cameras dict — leaving long-lived references (e.g. CameraState.camera_config) pointing at a copy that subsequent in-place mutations like apply_section_update can never reach. setdefault collapses everyone onto the first writer's object so attribute mutations propagate to every consumer in this process. * rebuild ffmpeg commands on detect update Rebuild the cached ffmpeg cmd so the next process spawn picks up new resolution/fps. Running cameras keep their existing cmd (ffmpeg_cmds is only read at process startup); replay cameras are recycled by CameraMaintainer to pick up the rebuilt cmd * drop stale shm cache refs when cached segment size doesn't match requested shape The cached SharedMemoryFrameManager reference can point at a segment whose size no longer matches the requested shape — the segment was unlinked and recreated at a different size in a camera add/remove cycle. This catches both a resolution increase (cached too small) and a decrease (cached too large, pointing at an orphaned inode whose stale bytes would otherwise be misinterpreted at the new shape, producing distorted/miscolored YUV frames). After reopening, if the OS-level segment still doesn't match the requested shape we're in a transient mid-recreate state — either the maintainer hasn't allocated the new segment yet (size too small) or we opened a pre-recycle segment (size too big). Either way, skip the frame and don't cache the mismatched ref. * recycle replay camera on detect update * discard tracked-object state when detect resolution changes mid-session When detect resolution changes mid-session every tracked object we hold was localized against the old pixel grid. Their boxes no longer correspond to anything in the new frame, and the `end` callback that fires when their IDs disappear from the new detect process's detections publishes those stale boxes to consumers (LPR, snapshot crop) that slice the new frame and crash on empty arrays. Drop the tracked-object state on a shape change so no stale boxes ever cross the CameraState boundary. Belt-and-suspenders: also drop any incoming batch whose boxes exceed the current detect resolution. These are in-flight queue entries from the pre-recycle detect process that beat the new detect process to the queue; processing them would re-introduce stale-resolution tracked objects we just dropped above. The per-camera detect process clamps legitimate boxes to detect.width-1 / detect.height-1, so any coord beyond that is unambiguously stale. * rebuild motion and object filter masks on detect resolution change Apply the detect update first so frame_shape reflects the new resolution before we rebuild dependents. Motion's rasterized_mask is sized to frame_shape at construction. When detect resolution changes we must rebuild RuntimeMotionConfig so the mask matches the new frame size; otherwise consumers like the LPR processor and motion detector hit a shape mismatch when they index frames with the stale mask. Same story for per-object filter masks — rebuild RuntimeFilterConfig at the new frame_shape so the merged global+per-object masks they hold match what they'll be indexed against. * republish motion and objects on in-memory detect resize A detect resolution change also invalidates the rasterized masks on motion and per-object filters. apply_section_update has rebuilt them at the new frame_shape; publish them too so other processes replace their old values. * add test * frontend * add refresh topic for camera maintainer recycle action The maintainer's recycle branch is doing an action (recycle the camera) in response to a section-level signal. Introduce a CameraConfigUpdateEnum.refresh case as an explicit action signal — the maintainer subscribes to refresh instead of detect, parallel with add and remove. Publishers fire refresh alongside detect when a recycle is needed; section-level subscribers keep their existing topic. Since no main-process subscriber listens for detect anymore, the refresh handler calls recreate_ffmpeg_cmds() explicitly so the shared CameraConfig's ffmpeg_cmds is rebuilt before the new subprocesses spawn. * factor stale-resolution state drop into a CameraState method --- frigate/api/app.py | 27 +++ frigate/camera/maintainer.py | 54 ++++++ frigate/camera/state.py | 60 ++++++- frigate/config/camera/updater.py | 5 +- frigate/test/test_camera_maintainer.py | 79 +++++++++ .../test/test_shared_memory_frame_manager.py | 156 ++++++++++++++++++ frigate/util/config.py | 28 ++++ frigate/util/image.py | 21 ++- .../config-form/section-configs/detect.ts | 19 +++ .../config-form/sections/BaseSection.tsx | 7 +- web/src/pages/Replay.tsx | 12 ++ 11 files changed, 454 insertions(+), 14 deletions(-) create mode 100644 frigate/test/test_camera_maintainer.py create mode 100644 frigate/test/test_shared_memory_frame_manager.py diff --git a/frigate/api/app.py b/frigate/api/app.py index cc5adc6f65..179c7fb90a 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -750,6 +750,33 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo settings, ) + # detect resize also republishes motion + objects so other + # processes pick up the rebuilt masks, and fires refresh so + # the camera maintainer recycles the camera process to pick + # up the new ffmpeg cmd / SHM sizing + if field == "detect": + cam_cfg = config.cameras.get(camera) + if cam_cfg is not None: + if cam_cfg.motion is not None: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.motion, camera + ), + cam_cfg.motion, + ) + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.objects, camera + ), + cam_cfg.objects, + ) + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.refresh, camera + ), + cam_cfg, + ) + return JSONResponse( content={"success": True, "message": "Config applied in-memory"}, status_code=200, diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index c4ddc51e89..ea8df7bff0 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -14,6 +14,7 @@ from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateSubscriber, ) +from frigate.const import REPLAY_CAMERA_PREFIX from frigate.models import Regions from frigate.util.builtin import empty_and_close_queue from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory @@ -50,6 +51,7 @@ class CameraMaintainer(threading.Thread): [ CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.remove, + CameraConfigUpdateEnum.refresh, ], ) self.shm_count = self.__calculate_shm_frame_count() @@ -202,6 +204,25 @@ class CameraMaintainer(threading.Thread): capture_process.terminate() capture_process.join() + def __unlink_camera_frame_slots(self, camera: str) -> None: + """Drop the camera's per-frame YUV SHM segments from this + process's frame_manager and unlink them at the OS level. + + Safe to call after the camera's capture/processor subprocesses + have been joined — they no longer hold mappings, so unlink frees + the segments immediately. Other long-lived processes that opened + these slots will continue using their existing mappings until + they call frame_manager.get with a shape that no longer fits + (the get path drops and reopens stale refs). + """ + prefix = f"{camera}_frame" + names = [n for n in list(self.frame_manager.shm_store) if n.startswith(prefix)] + for name in names: + try: + self.frame_manager.delete(name) + except Exception as exc: + logger.debug("Could not unlink SHM %s: %s", name, exc) + def __stop_camera_process(self, camera: str) -> None: camera_process = self.camera_processes.get(camera) if camera_process is not None: @@ -253,12 +274,45 @@ class CameraMaintainer(threading.Thread): for camera in updated_cameras: self.__stop_camera_capture_process(camera) self.__stop_camera_process(camera) + self.__unlink_camera_frame_slots(camera) self.capture_processes.pop(camera, None) self.camera_processes.pop(camera, None) self.camera_stop_events.pop(camera, None) self.region_grids.pop(camera, None) self.camera_metrics.pop(camera, None) self.ptz_metrics.pop(camera, None) + elif update_type == CameraConfigUpdateEnum.refresh.name: + # Recycle replay cameras so detect width/height/fps + # propagate through ffmpeg args, SHM sizing, and the + # region grid. Regular cameras detect change still + # requires a full restart. + for camera in updated_cameras: + if not camera.startswith(REPLAY_CAMERA_PREFIX): + continue + + new_config = self.update_subscriber.camera_configs.get(camera) + if new_config is None: + # remove arrived in the same batch + continue + + if ( + camera not in self.camera_processes + and camera not in self.capture_processes + ): + continue + + # rebuild ffmpeg cmds on the shared config so the + # new subprocesses spawn with current args + new_config.recreate_ffmpeg_cmds() + + self.__stop_camera_capture_process(camera) + self.__stop_camera_process(camera) + self.__unlink_camera_frame_slots(camera) + self.capture_processes.pop(camera, None) + self.camera_processes.pop(camera, None) + + self.__start_camera_processor(camera, new_config, runtime=True) + self.__start_camera_capture(camera, new_config, runtime=True) # ensure the capture processes are done for camera in self.capture_processes.keys(): diff --git a/frigate/camera/state.py b/frigate/camera/state.py index 8d0b586022..f35a3eaa56 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -45,6 +45,7 @@ class CameraState: self.frame_cache: dict[float, dict[str, Any]] = {} self.zone_objects: defaultdict[str, list[Any]] = defaultdict(list) self._current_frame = np.zeros(self.camera_config.frame_shape_yuv, np.uint8) + self._last_frame_shape: tuple[int, int] = self.camera_config.frame_shape_yuv self.current_frame_lock = threading.Lock() self.current_frame_time = 0.0 self.motion_boxes: list[tuple[int, int, int, int]] = [] @@ -303,6 +304,42 @@ class CameraState: def on(self, event_type: str, callback: Callable[..., Any]) -> None: self.callbacks[event_type].append(callback) + def _discard_stale_resolution_state( + self, current_detections: dict[str, dict[str, Any]] + ) -> bool: + """Drop tracked state when the camera's detect resolution has + changed, and signal the caller to skip this batch if it contains + out-of-bounds boxes from the pre-recycle detect process. + + Returns True when the batch should be skipped entirely. + """ + # detect resolution changed — drop tracked state so old-grid + # boxes don't leak through end-callbacks + current_shape = self.camera_config.frame_shape_yuv + if current_shape != self._last_frame_shape: + logger.debug( + f"{self.name}: detect resolution changed {self._last_frame_shape} -> {current_shape}, dropping tracked state" + ) + with self.current_frame_lock: + self.tracked_objects.clear() + self.motion_boxes = [] + self.regions = [] + self._last_frame_shape = current_shape + + # drop in-flight batches from the pre-recycle detect process + # whose boxes exceed the current detect resolution + detect = self.camera_config.detect + if detect.width is not None and detect.height is not None: + for obj in current_detections.values(): + box = obj.get("box") + if box and (box[2] > detect.width or box[3] > detect.height): + logger.debug( + f"{self.name}: dropping stale-resolution detection batch (box {box} exceeds {detect.width}x{detect.height})" + ) + return True + + return False + def update( self, frame_name: str, @@ -311,6 +348,9 @@ class CameraState: motion_boxes: list[tuple[int, int, int, int]], regions: list[tuple[int, int, int, int]], ) -> None: + if self._discard_stale_resolution_state(current_detections): + return + current_frame = self.frame_manager.get( frame_name, self.camera_config.frame_shape_yuv ) @@ -332,14 +372,18 @@ class CameraState: current_detections[id], ) - # add initial frame to frame cache - logger.debug( - f"{self.name}: New object, adding {frame_time} to frame cache for {id}" - ) - self.frame_cache[frame_time] = { - "frame": np.copy(current_frame), # type: ignore[arg-type] - "object_id": id, - } + # Skip caching when the frame buffer isn't readable — e.g. + # frame_manager.get returned None because the SHM segment was + # unlinked or hasn't been recreated yet during a camera + # add/remove cycle. + if current_frame is not None: + logger.debug( + f"{self.name}: New object, adding {frame_time} to frame cache for {id}" + ) + self.frame_cache[frame_time] = { + "frame": np.copy(current_frame), + "object_id": id, + } # save initial thumbnail data and best object thumbnail_data = { diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py index 95092da08b..b475f42157 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -26,6 +26,7 @@ class CameraConfigUpdateEnum(str, Enum): object_genai = "object_genai" onvif = "onvif" record = "record" + refresh = "refresh" # signals the camera maintainer to recycle the camera process remove = "remove" # for removing a camera review = "review" review_genai = "review_genai" @@ -84,8 +85,8 @@ class CameraConfigUpdateSubscriber: self, camera: str, update_type: CameraConfigUpdateEnum, updated_config: Any ) -> None: if update_type == CameraConfigUpdateEnum.add: - self.config.cameras[camera] = updated_config - self.camera_configs[camera] = updated_config + shared = self.config.cameras.setdefault(camera, updated_config) + self.camera_configs[camera] = shared return elif update_type == CameraConfigUpdateEnum.remove: self.config.cameras.pop(camera, None) diff --git a/frigate/test/test_camera_maintainer.py b/frigate/test/test_camera_maintainer.py new file mode 100644 index 0000000000..c03d965784 --- /dev/null +++ b/frigate/test/test_camera_maintainer.py @@ -0,0 +1,79 @@ +"""Tests for CameraMaintainer SHM cleanup on camera remove. + +Regression coverage for the case where a camera is removed and then a +new camera is added with the same name. Without unlinking the per-frame +YUV SHM slots, the maintainer's frame_manager.create call hits +FileExistsError and falls back to reopening the existing segment at the +*old* size, which the new ffmpeg process then writes mismatched-size +frames into. +""" + +import unittest +from unittest.mock import MagicMock, patch + +from frigate.camera.maintainer import CameraMaintainer + + +class TestMaintainerUnlinkFrameSlotsOnRemove(unittest.TestCase): + def _make_maintainer(self) -> CameraMaintainer: + """Build a maintainer without invoking __init__ (avoids needing real + FrigateConfig, queues, multiprocessing manager, etc.). We're only + exercising the SHM-cleanup helper, so the surrounding init is + irrelevant.""" + maintainer = CameraMaintainer.__new__(CameraMaintainer) + maintainer.frame_manager = MagicMock() + return maintainer + + def test_unlinks_only_segments_with_matching_prefix(self) -> None: + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = { + "front_frame0": object(), + "front_frame1": object(), + "front_frame2": object(), + # Different camera; must not be touched. + "side_frame0": object(), + # Detector input/output buffers are sized by the model and + # cached by the long-lived DetectorRunner — must not be + # touched even when their owning camera is removed. + "front": object(), + "out-front": object(), + } + + # __name-mangled access from outside the class. + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + deleted = [c.args[0] for c in maintainer.frame_manager.delete.call_args_list] + self.assertEqual( + sorted(deleted), + ["front_frame0", "front_frame1", "front_frame2"], + ) + + def test_handles_camera_with_no_slots(self) -> None: + """Cameras that were removed before any frame slot was ever + created (e.g. cancelled during preparing_clip) should be a no-op.""" + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = {"other_frame0": object()} + + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + maintainer.frame_manager.delete.assert_not_called() + + def test_swallows_delete_errors(self) -> None: + """Unlink failures shouldn't abort the remove loop — best-effort.""" + maintainer = self._make_maintainer() + maintainer.frame_manager.shm_store = { + "front_frame0": object(), + "front_frame1": object(), + } + maintainer.frame_manager.delete.side_effect = OSError("simulated") + + # Both slots are attempted; the OSError on the first doesn't + # prevent the second from being tried. + with patch("frigate.camera.maintainer.logger"): + maintainer._CameraMaintainer__unlink_camera_frame_slots("front") + + self.assertEqual(maintainer.frame_manager.delete.call_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_shared_memory_frame_manager.py b/frigate/test/test_shared_memory_frame_manager.py new file mode 100644 index 0000000000..63c96f732d --- /dev/null +++ b/frigate/test/test_shared_memory_frame_manager.py @@ -0,0 +1,156 @@ +"""Tests for SharedMemoryFrameManager cache invalidation. + +Covers the case where a SHM segment is unlinked and recreated at a +different size across a camera add/remove cycle while a long-lived +in-process cache (e.g. TrackedObjectProcessor) still holds a ref to +the old, smaller segment. +""" + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + +import numpy as np + +from frigate.util.image import SharedMemoryFrameManager + + +def _fake_shm(size: int) -> SimpleNamespace: + """A minimal stand-in for UntrackedSharedMemory with .size and .buf.""" + return SimpleNamespace(size=size, buf=bytearray(size), close=lambda: None) + + +class TestSharedMemoryFrameManagerGet(unittest.TestCase): + def test_get_reopens_when_cached_segment_is_smaller_than_shape(self) -> None: + """A cached ref to an older smaller segment must be dropped and the + current (correctly sized) segment reopened. Without this, np.ndarray + would raise "buffer is too small for requested array" when the + in-memory cache pointed at an old SHM after a same-name resize.""" + manager = SharedMemoryFrameManager() + + small = _fake_shm(size=100) + current = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = small + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=current): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (50, 50)) + self.assertIs(manager.shm_store["cam_frame0"], current) + + def test_get_reopens_when_cached_segment_is_larger_than_shape(self) -> None: + """Symmetric to the smaller-cache case: when detect resolution drops, + the SHM is unlinked and recreated at a smaller size. A cached ref to + the old, larger segment still satisfies any size check but points at + an orphaned inode whose stale bytes get reinterpreted at the new + shape — producing miscolored, distorted YUV frames downstream. Drop + the cache so we reopen by name and bind to the current segment.""" + manager = SharedMemoryFrameManager() + + old_large = _fake_shm(size=10_000) + current = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = old_large + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=current): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (50, 50)) + self.assertIs(manager.shm_store["cam_frame0"], current) + + def test_get_keeps_cached_segment_when_size_matches(self) -> None: + """Don't pay the reopen cost when the cached ref is the right size.""" + manager = SharedMemoryFrameManager() + + cached = _fake_shm(size=2_500) + manager.shm_store["cam_frame0"] = cached + + with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls: + arr = manager.get("cam_frame0", (50, 50)) + untracked_shm_cls.assert_not_called() + + self.assertIsNotNone(arr) + self.assertIs(manager.shm_store["cam_frame0"], cached) + + def test_get_opens_fresh_when_no_cache_entry(self) -> None: + manager = SharedMemoryFrameManager() + fresh = _fake_shm(size=2_500) + + with patch("frigate.util.image.UntrackedSharedMemory", return_value=fresh): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNotNone(arr) + self.assertIs(manager.shm_store["cam_frame0"], fresh) + + def test_get_returns_none_when_segment_missing(self) -> None: + manager = SharedMemoryFrameManager() + + with patch( + "frigate.util.image.UntrackedSharedMemory", + side_effect=FileNotFoundError, + ): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNone(arr) + + def test_get_returns_none_when_reopened_segment_is_still_too_small(self) -> None: + """Race during a same-name SHM recreate: cache is stale, we reopen + by name, but the maintainer hasn't allocated the new segment yet — + the reopened ref is also too small. Skip the frame (return None) + rather than crash on np.ndarray.""" + manager = SharedMemoryFrameManager() + + small_cached = _fake_shm(size=100) + still_small_after_reopen = _fake_shm(size=100) + manager.shm_store["cam_frame0"] = small_cached + + with patch( + "frigate.util.image.UntrackedSharedMemory", + return_value=still_small_after_reopen, + ): + arr = manager.get("cam_frame0", (50, 50)) + + self.assertIsNone(arr) + # Don't cache the too-small reopened ref — next call will re-open + # once the maintainer has finished recreating the segment. + self.assertNotIn("cam_frame0", manager.shm_store) + + def test_get_handles_n_dimensional_shape(self) -> None: + """np.prod must be used (not raw multiplication) for tuple shapes.""" + manager = SharedMemoryFrameManager() + # YUV-shaped frame: (height * 3/2, width) for 1920x1080 = 3,110,400 + big_enough = _fake_shm(size=3_110_400) + manager.shm_store["cam_frame0"] = big_enough + + with patch("frigate.util.image.UntrackedSharedMemory") as untracked_shm_cls: + arr = manager.get("cam_frame0", (1620, 1920)) + untracked_shm_cls.assert_not_called() + + self.assertIsNotNone(arr) + self.assertEqual(arr.shape, (1620, 1920)) + + +class TestSharedMemoryFrameManagerGetRecreatesLargerSegment(unittest.TestCase): + """End-to-end-style: simulates the full unlink-and-recreate cycle.""" + + def test_segment_grows_then_get_succeeds(self) -> None: + manager = SharedMemoryFrameManager() + + # Phase 1: existing camera at 320x240 YUV — 320 * 240 * 1.5 = 115_200 + small = _fake_shm(size=115_200) + manager.shm_store["cam_frame0"] = small + arr_small = np.ndarray((360, 320), dtype=np.uint8, buffer=small.buf) + self.assertEqual(arr_small.shape, (360, 320)) + + # Phase 2: restart at 1920x1080 — new SHM segment, larger size. + large = _fake_shm(size=3_110_400) + with patch("frigate.util.image.UntrackedSharedMemory", return_value=large): + arr_large = manager.get("cam_frame0", (1620, 1920)) + + self.assertIsNotNone(arr_large) + self.assertEqual(arr_large.shape, (1620, 1920)) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/util/config.py b/frigate/util/config.py index e6e3f09666..5e5d2a0fc8 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -788,6 +788,34 @@ def apply_section_update(camera_config, section: str, update: dict) -> Optional[ ) camera_config.objects = new_objects + elif section == "detect": + # apply detect first so frame_shape reflects the new resolution + # before we rebuild mask-dependent runtime configs below + merged = deep_merge(current.model_dump(), update, override=True) + camera_config.detect = current.__class__.model_validate(merged) + + new_frame_shape = camera_config.frame_shape + + # rebuild motion's rasterized_mask at the new frame_shape + if camera_config.motion is not None: + camera_config.motion = RuntimeMotionConfig( + frame_shape=new_frame_shape, + **camera_config.motion.model_dump(exclude_unset=True), + ) + + # rebuild per-object filter masks at the new frame_shape + for obj_name, filt in camera_config.objects.filters.items(): + merged_mask = dict(filt.mask) + if camera_config.objects.mask: + for gid, gmask in camera_config.objects.mask.items(): + merged_mask[f"global_{gid}"] = gmask + + camera_config.objects.filters[obj_name] = RuntimeFilterConfig( + frame_shape=new_frame_shape, + mask=merged_mask, + **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), + ) + else: merged = deep_merge(current.model_dump(), update, override=True) setattr(camera_config, section, current.__class__.model_validate(merged)) diff --git a/frigate/util/image.py b/frigate/util/image.py index 2d2133c6b8..d2832d97a0 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -1089,10 +1089,25 @@ class SharedMemoryFrameManager(FrameManager): def get(self, name: str, shape) -> Optional[np.ndarray]: try: - if name in self.shm_store: - shm = self.shm_store[name] - else: + required = int(np.prod(shape)) + shm = self.shm_store.get(name) + if shm is not None and shm.size != required: + # stale cached ref from a same-name recreate — drop and reopen + try: + shm.close() + except Exception: + pass + self.shm_store.pop(name, None) + shm = None + if shm is None: shm = UntrackedSharedMemory(name=name) + if shm.size != required: + # mid-recreate: OS segment doesn't match shape yet; skip + try: + shm.close() + except Exception: + pass + return None self.shm_store[name] = shm return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) except FileNotFoundError: diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index 5bbd219822..964a802d3f 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -72,6 +72,25 @@ const detect: SectionConfigOverrides = { "max_disappeared", ], }, + replay: { + restartRequired: [], + fieldOrder: ["width", "height", "fps"], + fieldGroups: { + resolution: ["width", "height", "fps"], + }, + hiddenFields: [ + "enabled", + "enabled_in_config", + "min_initialized", + "max_disappeared", + "annotation_offset", + "stationary", + "interval", + "threshold", + "max_frames", + ], + advancedFields: [], + }, }; export default detect; diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 5bacd2d80c..4fbdf76625 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -1253,7 +1253,12 @@ export function ConfigSection({
    - {title} + + {title} + {showOverrideIndicator && effectiveLevel === "camera" && (profileOverridesSection || isOverridden) && diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index b2253130bc..a775ee4312 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -354,6 +354,18 @@ export default function Replay() {
    ) : (
    + Date: Fri, 22 May 2026 14:41:07 -0500 Subject: [PATCH 38/94] Tweaks (#23292) * add review padding to explore debug replay api calls * add semantic search model size widget disables model_size select with n/a text when an embeddings genai provider is selected * regenerate zone contours and per-zone filter masks on detect resolution change * treat null as a clear sentinel in buildOverrides so nullable field edits don't snap back * extract replay config sheet to new component * add validation and messages for detect settings --- frigate/api/app.py | 7 + frigate/util/config.py | 11 ++ web/public/locales/en/config/validation.json | 3 + web/public/locales/en/views/settings.json | 10 +- .../config-form/LiveFormDataContext.ts | 13 ++ .../config-form/section-configs/detect.ts | 44 +++++++ .../section-configs/semantic_search.ts | 17 +-- .../config-form/section-validations/detect.ts | 36 ++++++ .../config-form/section-validations/index.ts | 5 + .../config-form/sections/BaseSection.tsx | 111 ++++++++-------- .../config-form/theme/frigateTheme.ts | 2 + .../widgets/SemanticSearchModelSizeWidget.tsx | 57 +++++++++ .../components/menu/SearchResultActions.tsx | 5 +- .../overlay/DebugReplayConfigSheet.tsx | 120 ++++++++++++++++++ .../overlay/detail/DetailActionsMenu.tsx | 4 +- web/src/components/timeline/EventMenu.tsx | 5 +- web/src/pages/Replay.tsx | 107 +--------------- web/src/utils/configUtil.ts | 7 +- 18 files changed, 382 insertions(+), 182 deletions(-) create mode 100644 web/src/components/config-form/LiveFormDataContext.ts create mode 100644 web/src/components/config-form/section-validations/detect.ts create mode 100644 web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx create mode 100644 web/src/components/overlay/DebugReplayConfigSheet.tsx diff --git a/frigate/api/app.py b/frigate/api/app.py index 179c7fb90a..35eed2b9ce 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -770,6 +770,13 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo ), cam_cfg.objects, ) + if cam_cfg.zones: + request.app.config_publisher.publish_update( + CameraConfigUpdateTopic( + CameraConfigUpdateEnum.zones, camera + ), + cam_cfg.zones, + ) request.app.config_publisher.publish_update( CameraConfigUpdateTopic( CameraConfigUpdateEnum.refresh, camera diff --git a/frigate/util/config.py b/frigate/util/config.py index 5e5d2a0fc8..431c8bff53 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -816,6 +816,17 @@ def apply_section_update(camera_config, section: str, update: dict) -> Optional[ **filt.model_dump(exclude_unset=True, exclude={"mask", "raw_mask"}), ) + # Regenerate zone contours and per-zone filter masks at the new + # frame_shape so zone outlines and membership stay relative + for zone in camera_config.zones.values(): + if zone.filters: + for zone_obj_name, zone_filter in zone.filters.items(): + zone.filters[zone_obj_name] = RuntimeFilterConfig( + frame_shape=new_frame_shape, + **zone_filter.model_dump(exclude_unset=True), + ) + zone.generate_contour(new_frame_shape) + else: merged = deep_merge(current.model_dump(), update, override=True) setattr(camera_config, section, current.__class__.model_validate(merged)) diff --git a/web/public/locales/en/config/validation.json b/web/public/locales/en/config/validation.json index 6f3b5f6864..f3d98a65e9 100644 --- a/web/public/locales/en/config/validation.json +++ b/web/public/locales/en/config/validation.json @@ -28,5 +28,8 @@ "detectRequired": "At least one input stream must be assigned the 'detect' role.", "hwaccelDetectOnly": "Only the input stream with the detect role can define hardware acceleration arguments." } + }, + "detect": { + "dimensionMustBeEven": "Must be an even number." } } diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 7bb582120b..11fcd92123 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1543,6 +1543,9 @@ "builtIn": "Built-in Models", "genaiProviders": "GenAI Providers" }, + "semanticSearchModelSize": { + "notApplicable": "Not applicable for GenAI providers" + }, "review": { "title": "Review Settings" }, @@ -1791,7 +1794,9 @@ }, "detect": { "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended. Higher values may cause performance issues and will not provide any benefit.", - "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function." + "disabled": "Object detection is disabled. Snapshots, review items, and enrichments such as face recognition, license plate recognition, and Generative AI will not function.", + "resolutionShouldBeMultipleOfFour": "For best results, detect width and height should be multiples of 4. Other even values may produce visual artifacts or slight distortion in the detect stream.", + "aspectRatioMismatch": "The width and height you've entered don't match the aspect ratio of your current detect resolution. This may produce a stretched or distorted image." }, "objects": { "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated." @@ -1820,8 +1825,7 @@ "mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}." }, "semanticSearch": { - "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended.", - "modelSizeIgnoredForProvider": "Model size only applies to the built-in Jina models. This value will be ignored when using a GenAI embedding provider." + "jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended." } } } diff --git a/web/src/components/config-form/LiveFormDataContext.ts b/web/src/components/config-form/LiveFormDataContext.ts new file mode 100644 index 0000000000..10d9a3e82c --- /dev/null +++ b/web/src/components/config-form/LiveFormDataContext.ts @@ -0,0 +1,13 @@ +import { createContext } from "react"; +import type { ConfigSectionData } from "@/types/configForm"; + +// Mirrors the current section's in-flight form data so widgets can react +// to changes that RJSF wouldn't otherwise re-render them for. RJSF's +// Form memoizes SchemaField via deep equality and, in some transitions +// (notably reverting a field to its saved value), can skip re-rendering +// a widget even though the form data it depends on changed. useContext +// re-runs consumers directly on every provider value update, sidestepping +// that. +export const LiveFormDataContext = createContext( + null, +); diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index 964a802d3f..74d170edc6 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -11,6 +11,50 @@ const detect: SectionConfigOverrides = { condition: (ctx) => ctx.level === "camera" && ctx.formData?.enabled === false, }, + { + key: "detect-resolution-not-multiple-of-four", + messageKey: "configMessages.detect.resolutionShouldBeMultipleOfFour", + severity: "warning", + condition: (ctx) => { + const width = ctx.formData?.width as number | null | undefined; + const height = ctx.formData?.height as number | null | undefined; + const isEvenButNotFour = (v: unknown) => + typeof v === "number" && v % 2 === 0 && v % 4 !== 0; + return isEvenButNotFour(width) || isEvenButNotFour(height); + }, + }, + { + key: "detect-aspect-ratio-mismatch", + messageKey: "configMessages.detect.aspectRatioMismatch", + severity: "warning", + condition: (ctx) => { + const newWidth = ctx.formData?.width as number | null | undefined; + const newHeight = ctx.formData?.height as number | null | undefined; + if (typeof newWidth !== "number" || typeof newHeight !== "number") { + return false; + } + const saved = + ctx.level === "camera" + ? ctx.fullCameraConfig?.detect + : ctx.fullConfig?.detect; + const savedWidth = saved?.width; + const savedHeight = saved?.height; + if ( + typeof savedWidth !== "number" || + typeof savedHeight !== "number" || + savedWidth <= 0 || + savedHeight <= 0 + ) { + return false; + } + if (newWidth === savedWidth && newHeight === savedHeight) { + return false; + } + const newRatio = newWidth / newHeight; + const savedRatio = savedWidth / savedHeight; + return Math.abs(newRatio - savedRatio) > 0.01; + }, + }, ], fieldMessages: [ { diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts index 3f9bbfaec1..dde2b75531 100644 --- a/web/src/components/config-form/section-configs/semantic_search.ts +++ b/web/src/components/config-form/section-configs/semantic_search.ts @@ -29,28 +29,13 @@ const semanticSearch: SectionConfigOverrides = { ctx.formData?.model === "jinav2" && ctx.formData?.model_size === "small", }, - { - key: "model-size-ignored-for-provider", - field: "model_size", - messageKey: "configMessages.semanticSearch.modelSizeIgnoredForProvider", - severity: "info", - position: "after", - condition: (ctx) => { - const model = ctx.formData?.model; - return ( - typeof model === "string" && - model !== "" && - model !== "jinav1" && - model !== "jinav2" - ); - }, - }, ], uiSchema: { model: { "ui:widget": "semanticSearchModel", }, model_size: { + "ui:widget": "semanticSearchModelSize", "ui:options": { size: "xs", enumI18nPrefix: "modelSize" }, }, }, diff --git a/web/src/components/config-form/section-validations/detect.ts b/web/src/components/config-form/section-validations/detect.ts new file mode 100644 index 0000000000..7ecc805b72 --- /dev/null +++ b/web/src/components/config-form/section-validations/detect.ts @@ -0,0 +1,36 @@ +import type { FormValidation } from "@rjsf/utils"; +import type { TFunction } from "i18next"; +import { isJsonObject } from "@/lib/utils"; +import type { JsonObject } from "@/types/configForm"; + +export function validateDetectDimensions( + formData: unknown, + errors: FormValidation, + t: TFunction, +): FormValidation { + if (!isJsonObject(formData as JsonObject)) { + return errors; + } + + const data = formData as JsonObject; + const width = data.width; + const height = data.height; + + const widthErrors = errors.width as + | { addError?: (message: string) => void } + | undefined; + const heightErrors = errors.height as + | { addError?: (message: string) => void } + | undefined; + + const message = t("detect.dimensionMustBeEven", { ns: "config/validation" }); + + if (typeof width === "number" && width % 2 !== 0) { + widthErrors?.addError?.(message); + } + if (typeof height === "number" && height % 2 !== 0) { + heightErrors?.addError?.(message); + } + + return errors; +} diff --git a/web/src/components/config-form/section-validations/index.ts b/web/src/components/config-form/section-validations/index.ts index 31a02a1d10..33c02b1c7a 100644 --- a/web/src/components/config-form/section-validations/index.ts +++ b/web/src/components/config-form/section-validations/index.ts @@ -1,5 +1,6 @@ import type { FormValidation } from "@rjsf/utils"; import type { TFunction } from "i18next"; +import { validateDetectDimensions } from "./detect"; import { validateFfmpegInputRoles } from "./ffmpeg"; import { validateProxyRoleHeader } from "./proxy"; @@ -19,6 +20,10 @@ export function getSectionValidation({ level, t, }: SectionValidationOptions): SectionValidation | undefined { + if (sectionPath === "detect") { + return (formData, errors) => validateDetectDimensions(formData, errors, t); + } + if (sectionPath === "ffmpeg" && level === "camera") { return (formData, errors) => validateFfmpegInputRoles(formData, errors, t); } diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 4fbdf76625..b3261a5cc4 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -87,6 +87,7 @@ import type { import { useConfigMessages } from "@/hooks/use-config-messages"; import { ConfigMessageBanner } from "../ConfigMessageBanner"; import { FieldMessagesContext } from "../FieldMessagesContext"; +import { LiveFormDataContext } from "../LiveFormDataContext"; export interface SectionConfig { /** Field ordering within the section */ @@ -998,59 +999,63 @@ export function ConfigSection({
    - handleChange(data), - // For widgets that need access to full camera config (e.g., zone names) - fullCameraConfig: - effectiveLevel === "camera" && cameraName - ? config?.cameras?.[cameraName] - : undefined, - fullConfig: config, - // When rendering camera-level sections, provide the section path so - // field templates can look up keys under the `config/cameras` namespace - // When using a consolidated global namespace, keys are nested - // under the section name (e.g., `audio.label`) so provide the - // section prefix to templates so they can attempt `${section}.${field}` lookups. - sectionI18nPrefix: sectionPath, - t, - renderers: wrappedRenderers, - sectionDocs: sectionConfig.sectionDocs, - fieldDocs: sectionConfig.fieldDocs, - hiddenFields: effectiveHiddenFields, - restartRequired: sectionConfig.restartRequired, - requiresRestart, - isProfile: !!profileName, - }} - /> + + handleChange(data), + // For widgets that need access to full camera config (e.g., zone names) + fullCameraConfig: + effectiveLevel === "camera" && cameraName + ? config?.cameras?.[cameraName] + : undefined, + fullConfig: config, + // When rendering camera-level sections, provide the section path so + // field templates can look up keys under the `config/cameras` namespace + // When using a consolidated global namespace, keys are nested + // under the section name (e.g., `audio.label`) so provide the + // section prefix to templates so they can attempt `${section}.${field}` lookups. + sectionI18nPrefix: sectionPath, + t, + renderers: wrappedRenderers, + sectionDocs: sectionConfig.sectionDocs, + fieldDocs: sectionConfig.fieldDocs, + hiddenFields: effectiveHiddenFields, + restartRequired: sectionConfig.restartRequired, + requiresRestart, + isProfile: !!profileName, + }} + /> + {!embedded && ( diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index ae612d9ac7..ebc6b19b35 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -31,6 +31,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget"; import { CameraPathWidget } from "./widgets/CameraPathWidget"; import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget"; import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget"; +import { SemanticSearchModelSizeWidget } from "./widgets/SemanticSearchModelSizeWidget"; import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget"; import { FieldTemplate } from "./templates/FieldTemplate"; @@ -86,6 +87,7 @@ export const frigateTheme: FrigateTheme = { timezoneSelect: TimezoneSelectWidget, optionalField: OptionalFieldWidget, semanticSearchModel: SemanticSearchModelWidget, + semanticSearchModelSize: SemanticSearchModelSizeWidget, onvifProfile: OnvifProfileWidget, }, templates: { diff --git a/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx b/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx new file mode 100644 index 0000000000..4ee0019363 --- /dev/null +++ b/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx @@ -0,0 +1,57 @@ +// Disables model_size and shows "N/A" when a GenAI provider is selected. +// Reads model via LiveFormDataContext so it re-runs even when RJSF's +// SchemaField memoization would skip this widget. +import type { WidgetProps } from "@rjsf/utils"; +import { useContext, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { LiveFormDataContext } from "../../LiveFormDataContext"; +import { getSizedFieldClassName } from "../utils"; +import { SelectWidget } from "./SelectWidget"; + +export function SemanticSearchModelSizeWidget(props: WidgetProps) { + const { t } = useTranslation(["views/settings"]); + const liveFormData = useContext(LiveFormDataContext); + const model = liveFormData?.model; + const isProvider = + typeof model === "string" && + model !== "" && + model !== "jinav1" && + model !== "jinav2"; + + // Clear model_size while on a provider (buildOverrides converts to "" + // which the backend treats as "remove"). Restore the schema default + // when returning to a Jina model so the field isn't left empty. + const { value, onChange, schema } = props; + const schemaDefault = schema?.default as string | undefined; + useEffect(() => { + if (isProvider && value !== undefined) { + onChange(undefined); + } else if (!isProvider && value === undefined && schemaDefault) { + onChange(schemaDefault); + } + }, [isProvider, value, onChange, schemaDefault]); + + if (isProvider) { + const fieldClassName = getSizedFieldClassName(props.options ?? {}, "sm"); + return ( + + ); + } + + return ; +} diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 43a192dc9e..90a70ff5d9 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -1,5 +1,6 @@ import { useState, ReactNode, useCallback } from "react"; import { SearchResult } from "@/types/search"; +import { REVIEW_PADDING } from "@/types/review"; import { FrigateConfig } from "@/types/frigateConfig"; import { baseUrl } from "@/api/baseUrl"; import { toast } from "sonner"; @@ -94,8 +95,8 @@ export default function SearchResultActions({ axios .post("debug_replay/start", { camera: event.camera, - start_time: event.start_time, - end_time: event.end_time, + start_time: (event.start_time ?? 0) - REVIEW_PADDING, + end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING, }) .then((response) => { if (response.status === 202 || response.status === 200) { diff --git a/web/src/components/overlay/DebugReplayConfigSheet.tsx b/web/src/components/overlay/DebugReplayConfigSheet.tsx new file mode 100644 index 0000000000..ca558e43dd --- /dev/null +++ b/web/src/components/overlay/DebugReplayConfigSheet.tsx @@ -0,0 +1,120 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { LuSettings } from "react-icons/lu"; +import useSWR from "swr"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate"; +import { Button } from "@/components/ui/button"; +import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog"; +import { useConfigSchema } from "@/hooks/use-config-schema"; +import type { FrigateConfig } from "@/types/frigateConfig"; + +type DebugReplayConfigSheetProps = { + replayCamera: string | undefined; +}; + +export function DebugReplayConfigSheet({ + replayCamera, +}: DebugReplayConfigSheetProps) { + const { t } = useTranslation(["views/replay"]); + const configSchema = useConfigSchema(); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const [open, setOpen] = useState(false); + + return ( + + + {t("page.configuration")} + + } + title={t("page.configuration")} + titleClassName="text-lg font-semibold" + contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl" + content={ + <> +

    + {t("page.configurationDesc")} +

    + {configSchema == null ? ( +
    + +
    + ) : ( +
    + + + + {config?.face_recognition?.enabled && ( + + )} + {config?.lpr?.enabled && ( + + )} +
    + )} + + } + open={open} + onOpenChange={setOpen} + /> + ); +} diff --git a/web/src/components/overlay/detail/DetailActionsMenu.tsx b/web/src/components/overlay/detail/DetailActionsMenu.tsx index 789f396772..af83700a6f 100644 --- a/web/src/components/overlay/detail/DetailActionsMenu.tsx +++ b/web/src/components/overlay/detail/DetailActionsMenu.tsx @@ -63,8 +63,8 @@ export default function DetailActionsMenu({ axios .post("debug_replay/start", { camera: search.camera, - start_time: search.start_time, - end_time: search.end_time, + start_time: (search.start_time ?? 0) - REVIEW_PADDING, + end_time: (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING, }) .then((response) => { if (response.status === 202 || response.status === 200) { diff --git a/web/src/components/timeline/EventMenu.tsx b/web/src/components/timeline/EventMenu.tsx index 42efd2c97f..5c2798f2af 100644 --- a/web/src/components/timeline/EventMenu.tsx +++ b/web/src/components/timeline/EventMenu.tsx @@ -12,6 +12,7 @@ import { baseUrl } from "@/api/baseUrl"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Event } from "@/types/event"; +import { REVIEW_PADDING } from "@/types/review"; import { FrigateConfig } from "@/types/frigateConfig"; import { useCallback, useState } from "react"; import { useIsAdmin } from "@/hooks/use-is-admin"; @@ -58,8 +59,8 @@ export default function EventMenu({ axios .post("debug_replay/start", { camera: event.camera, - start_time: event.start_time, - end_time: event.end_time, + start_time: (event.start_time ?? 0) - REVIEW_PADDING, + end_time: (event.end_time ?? Date.now() / 1000) + REVIEW_PADDING, }) .then((response) => { if (response.status === 202 || response.status === 200) { diff --git a/web/src/pages/Replay.tsx b/web/src/pages/Replay.tsx index a775ee4312..f43e52ee22 100644 --- a/web/src/pages/Replay.tsx +++ b/web/src/pages/Replay.tsx @@ -27,7 +27,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { PlatformAwareSheet } from "@/components/overlay/dialog/PlatformAwareDialog"; +import { DebugReplayConfigSheet } from "@/components/overlay/DebugReplayConfigSheet"; import { useCameraActivity } from "@/hooks/use-camera-activity"; import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; @@ -40,16 +40,14 @@ import { Progress } from "@/components/ui/progress"; import { ObjectType } from "@/types/ws"; import { useJobStatus } from "@/api/ws"; import WsMessageFeed from "@/components/ws/WsMessageFeed"; -import { ConfigSectionTemplate } from "@/components/config-form/sections/ConfigSectionTemplate"; -import { LuExternalLink, LuInfo, LuSettings } from "react-icons/lu"; +import { LuExternalLink, LuInfo } from "react-icons/lu"; import { LuSquare } from "react-icons/lu"; import { MdReplay } from "react-icons/md"; import { isDesktop, isMobile } from "react-device-detect"; import Logo from "@/components/Logo"; import { Separator } from "@/components/ui/separator"; import { useDocDomain } from "@/hooks/use-doc-domain"; -import { useConfigSchema } from "@/hooks/use-config-schema"; import DebugDrawingLayer from "@/components/overlay/DebugDrawingLayer"; import { IoMdArrowRoundBack } from "react-icons/io"; @@ -125,7 +123,6 @@ export default function Replay() { }); const { payload: replayJob } = useJobStatus("debug_replay"); - const configSchema = useConfigSchema(); const [isInitializing, setIsInitializing] = useState(true); // Refresh status immediately on mount to avoid showing "no session" briefly @@ -139,7 +136,6 @@ export default function Replay() { const [options, setOptions] = useState(DEFAULT_OPTIONS); const [isStopping, setIsStopping] = useState(false); - const [configDialogOpen, setConfigDialogOpen] = useState(false); const searchParams = useMemo(() => { const params = new URLSearchParams(); @@ -327,103 +323,8 @@ export default function Replay() { )}
    - - - - {t("page.configuration")} - - - } - title={t("page.configuration")} - titleClassName="text-lg font-semibold" - contentClassName="scrollbar-container flex flex-col gap-0 overflow-y-auto px-6 pb-6 sm:max-w-xl md:max-w-2xl xl:max-w-3xl" - content={ - <> -

    - {t("page.configurationDesc")} -

    - {configSchema == null ? ( -
    - -
    - ) : ( -
    - - - - {config?.face_recognition?.enabled && ( - - )} - {config?.lpr?.enabled && ( - - )} -
    - )} - - } - open={configDialogOpen} - onOpenChange={setConfigDialogOpen} + diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index cb7f6f52b6..3ebe96a63b 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -229,7 +229,12 @@ export function buildOverrides( const result: JsonObject = {}; for (const [key, value] of Object.entries(currentObj)) { - if (value === undefined && baseObj && baseObj[key] !== undefined) { + if ( + (value === undefined || value === null) && + baseObj && + baseObj[key] !== undefined && + baseObj[key] !== null + ) { result[key] = ""; continue; } From ec44398b1c3e1c5fbf2d1177d6f1c78c8c8f108a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 24 May 2026 07:48:52 -0500 Subject: [PATCH 39/94] Miscellaneous fixes (#23295) * filter motion review by allowed cameras * filter alertCameras by allowed cameras so the recent alerts query for restricted roles doesn't reference cameras they can't access * skip data streams in chapter exports to avoid ffmpeg segfault * formatting * restrict debug replay UI entry points to admin users * Adjust default iGPU name when it can't be found * Fix when model tries to request an invalid camera * Improve prompt * add collapsible main nav items in settings --------- Co-authored-by: Nicolas Mowen --- frigate/api/chat.py | 21 +- frigate/genai/prompts.py | 11 +- frigate/record/export.py | 4 +- frigate/util/services.py | 2 +- .../components/menu/SearchResultActions.tsx | 14 +- .../components/overlay/ActionsDropdown.tsx | 10 +- .../overlay/MobileReviewSettingsDrawer.tsx | 4 +- .../overlay/detail/DetailActionsMenu.tsx | 14 +- web/src/components/timeline/EventMenu.tsx | 14 +- web/src/pages/Settings.tsx | 214 ++++++++++++------ web/src/views/events/EventView.tsx | 29 +-- web/src/views/live/LiveDashboardView.tsx | 10 +- web/src/views/recording/RecordingView.tsx | 20 +- 13 files changed, 253 insertions(+), 114 deletions(-) diff --git a/frigate/api/chat.py b/frigate/api/chat.py index c7d197bf91..4e6bdbd3b4 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -547,9 +547,21 @@ async def _execute_get_live_context( camera: str, allowed_cameras: List[str], ) -> Dict[str, Any]: + # Reject wildcards explicitly so models retry with a real camera name + # instead of silently fanning out across every camera. + if camera in ("*", "all"): + return { + "error": ( + "get_live_context requires a single camera name; wildcards " + "are not supported. Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } + if camera not in allowed_cameras: return { "error": f"Camera '{camera}' not found or access denied", + "available_cameras": allowed_cameras, } if camera not in request.app.frigate_config.cameras: @@ -721,7 +733,14 @@ async def _execute_tool_internal( "Arguments: %s", json.dumps(arguments), ) - return {"error": "Camera parameter is required"} + return { + "error": ( + "get_live_context requires a single camera name; " + "wildcards and empty values are not supported. " + "Call this tool once per camera." + ), + "available_cameras": allowed_cameras, + } return await _execute_get_live_context(request, camera, allowed_cameras) elif tool_name == "start_camera_watch": return await _execute_start_camera_watch(request, arguments) diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py index 93e19209bf..af6ddab889 100644 --- a/frigate/genai/prompts.py +++ b/frigate/genai/prompts.py @@ -518,16 +518,21 @@ def get_tool_definitions( "function": { "name": "get_live_context", "description": ( - "Get the current live image and detection information for a camera: objects being tracked, " + "Get the current live image and detection information for a single camera: objects being tracked, " "zones, timestamps. Use this to understand what is visible in the live view. " - "Call this when answering questions about what is happening right now on a specific camera." + "Call this when answering questions about what is happening right now on a specific camera. " + "Operates on one camera at a time; call the tool again for each additional camera. " + "Wildcards and empty values are not accepted." ), "parameters": { "type": "object", "properties": { "camera": { "type": "string", - "description": "Camera name to get live context for.", + "description": ( + "Exact name of a single camera to get live context for. " + "Wildcards (e.g. '*', 'all') and empty strings are not accepted." + ), }, }, "required": ["camera"], diff --git a/frigate/record/export.py b/frigate/record/export.py index 9f571a5a5c..3a943cb3fe 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -579,7 +579,9 @@ class RecordingExporter(threading.Thread): else: chapters_path = self._build_chapter_metadata_file(recordings) chapter_args = ( - f" -i {chapters_path} -map 0 -map_metadata 1" if chapters_path else "" + f" -i {chapters_path} -map 0 -dn -map_metadata 1" + if chapters_path + else "" ) ffmpeg_cmd = ( f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input}{chapter_args} -c copy -movflags +faststart" diff --git a/frigate/util/services.py b/frigate/util/services.py index 4a715608e2..a5b1af8249 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -478,7 +478,7 @@ def get_intel_gpu_stats( overall_pct = min(100.0, compute_pct + dec_pct) entry: dict[str, Any] = { - "name": names.get(pdev) or f"Intel GPU {pdev}", + "name": names.get(pdev) or "Intel iGPU", "vendor": "intel", "gpu": f"{round(overall_pct, 2)}%", "mem": "-%", diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 90a70ff5d9..c7af518932 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -130,9 +130,15 @@ export default function SearchResultActions({ }, ); } else { - toast.error(t("dialog.toast.error", { error: errorMessage }), { - position: "top-center", - }); + toast.error( + t("dialog.toast.error", { + ns: "views/replay", + error: errorMessage, + }), + { + position: "top-center", + }, + ); } }) .finally(() => { @@ -206,7 +212,7 @@ export default function SearchResultActions({ {t("itemMenu.addTrigger.label")} )} - {searchResult.has_clip && ( + {isAdmin && searchResult.has_clip && ( void; + onDebugReplayClick?: () => void; onExportClick: () => void; onShareTimestampClick: () => void; }; @@ -42,9 +42,11 @@ export default function ActionsDropdown({ {t("recording.shareTimestamp.label", { ns: "components/dialog" })} - - {t("title", { ns: "views/replay" })} - + {onDebugReplayClick && ( + + {t("title", { ns: "views/replay" })} + + )} ); diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 2ad2067462..c2daf6d051 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -29,6 +29,7 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { StartExportResponse } from "@/types/export"; import { ShareTimestampContent } from "./ShareTimestampDialog"; +import { useIsAdmin } from "@/hooks/use-is-admin"; type DrawerMode = | "none" @@ -109,6 +110,7 @@ export default function MobileReviewSettingsDrawer({ "views/replay", "common", ]); + const isAdmin = useIsAdmin(); const navigate = useNavigate(); const [drawerMode, setDrawerMode] = useState("none"); const [exportTab, setExportTab] = useState("export"); @@ -388,7 +390,7 @@ export default function MobileReviewSettingsDrawer({ {t("filter")} )} - {features.includes("debug-replay") && ( + {isAdmin && features.includes("debug-replay") && (
    ); } @@ -613,6 +617,39 @@ export default function Settings() { const [sectionStatusByKey, setSectionStatusByKey] = useState< Partial> >({}); + const [collapsedGroups, setCollapsedGroups] = useState>( + () => + // all collapsed by default + new Set( + settingsGroups.filter((g) => g.items.length > 1).map((g) => g.label), + ), + ); + + const toggleGroupCollapsed = useCallback((label: string) => { + setCollapsedGroups((prev) => { + const next = new Set(prev); + if (next.has(label)) { + next.delete(label); + } else { + next.add(label); + } + return next; + }); + }, []); + + // Auto-expand the group containing the active page whenever pageToggle changes + useEffect(() => { + const containingGroup = settingsGroups.find((group) => + group.items.some((item) => item.key === pageToggle), + ); + if (!containingGroup) return; + setCollapsedGroups((prev) => { + if (!prev.has(containingGroup.label)) return prev; + const next = new Set(prev); + next.delete(containingGroup.label); + return next; + }); + }, [pageToggle]); const { data: config } = useSWR("config"); const { data: profilesData } = useSWR("profiles"); @@ -1611,34 +1648,49 @@ export default function Settings() { visibleSettingsViews.includes(item.key as SettingsType), ); if (filteredItems.length === 0) return null; + const isMultiItem = filteredItems.length > 1; + const renderedExpanded = + !isMultiItem || !collapsedGroups.has(group.label); + const items = filteredItems.map((item) => ( + { + if ( + !isAdmin && + !ALLOWED_VIEWS_FOR_VIEWER.includes(key as SettingsType) + ) { + setPageToggle("uiSettings"); + } else { + setPageToggle(key as SettingsType); + } + setContentMobileOpen(true); + }} + /> + )); return (
    - {filteredItems.length > 1 && ( -

    -
    {t("menu." + group.label)}
    -

    + {isMultiItem ? ( + toggleGroupCollapsed(group.label)} + > + +
    {t("menu." + group.label)}
    + +
    + {items} +
    + ) : ( + items )} - {filteredItems.map((item) => ( - { - if ( - !isAdmin && - !ALLOWED_VIEWS_FOR_VIEWER.includes( - key as SettingsType, - ) - ) { - setPageToggle("uiSettings"); - } else { - setPageToggle(key as SettingsType); - } - setContentMobileOpen(true); - }} - /> - ))}
    ); })} @@ -1940,48 +1992,74 @@ export default function Settings() { ) : ( - <> - pageToggle === item.key, - ) - ? "text-primary" - : "text-sidebar-foreground/80", - )} - > -
    {t("menu." + group.label)}
    -
    - - {filteredItems.map((item) => ( - - { - if ( - !isAdmin && - !ALLOWED_VIEWS_FOR_VIEWER.includes( - item.key as SettingsType, - ) - ) { - setPageToggle("uiSettings"); - } else { - setPageToggle(item.key as SettingsType); - } - }} - > -
    - {renderMenuItemLabel( - item.key as SettingsType, + (() => { + const hasActiveItem = filteredItems.some( + (item) => pageToggle === item.key, + ); + const renderedExpanded = !collapsedGroups.has( + group.label, + ); + return ( + + toggleGroupCollapsed(group.label) + } + > + + +
    {t("menu." + group.label)}
    + - - - ))} - - + /> +
    +
    + + + {filteredItems.map((item) => ( + + { + if ( + !isAdmin && + !ALLOWED_VIEWS_FOR_VIEWER.includes( + item.key as SettingsType, + ) + ) { + setPageToggle("uiSettings"); + } else { + setPageToggle( + item.key as SettingsType, + ); + } + }} + > +
    + {renderMenuItemLabel( + item.key as SettingsType, + )} +
    +
    +
    + ))} +
    +
    +
    + ); + })() )} ); diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index ba9cdf0328..86ab910374 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -66,6 +66,7 @@ import SummaryTimeline from "@/components/timeline/SummaryTimeline"; import { RecordingStartingPoint } from "@/types/record"; import VideoControls from "@/components/player/VideoControls"; import { TimeRange } from "@/types/timeline"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useCameraMotionNextTimestamp, useCameraMotionOnlyRanges, @@ -1008,27 +1009,29 @@ function MotionReview({ const { t } = useTranslation(["views/events", "common"]); const segmentDuration = 30; const { data: config } = useSWR("config"); + const allowedCameras = useAllowedCameras(); const reviewCameras = useMemo(() => { if (!config) { return []; } - let cameras; - if (!filter || !filter.cameras) { - cameras = Object.values(config.cameras).filter( - (cam) => !isReplayCamera(cam.name), - ); - } else { - const filteredCams = filter.cameras; - - cameras = Object.values(config.cameras).filter( - (cam) => filteredCams.includes(cam.name) && !isReplayCamera(cam.name), - ); - } + const selectedCams = filter?.cameras; + const cameras = Object.values(config.cameras).filter((cam) => { + if (isReplayCamera(cam.name)) { + return false; + } + if (!allowedCameras.includes(cam.name)) { + return false; + } + if (selectedCams && !selectedCams.includes(cam.name)) { + return false; + } + return true; + }); return cameras.sort((a, b) => a.ui.order - b.ui.order); - }, [config, filter]); + }, [config, filter, allowedCameras]); const videoPlayersRef = useRef<{ [camera: string]: PreviewController }>({}); diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 716ffed041..629bf30b87 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -13,6 +13,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useAllowedCameras } from "@/hooks/use-allowed-cameras"; import { useUserPersistence } from "@/hooks/use-user-persistence"; import { AllGroupsStreamingSettings, @@ -90,6 +91,7 @@ export default function LiveDashboardView({ // recent events const eventUpdate = useFrigateReviews(); + const allowedCameras = useAllowedCameras(); const alertCameras = useMemo(() => { if (!config) { @@ -98,14 +100,16 @@ export default function LiveDashboardView({ if (cameraGroup == "default") { return Object.values(config.cameras) - .filter((cam) => cam.ui.dashboard) + .filter((cam) => cam.ui.dashboard && allowedCameras.includes(cam.name)) .map((cam) => cam.name) .join(","); } if (includeBirdseye && cameras.length == 0) { return Object.values(config.cameras) - .filter((cam) => cam.birdseye.enabled) + .filter( + (cam) => cam.birdseye.enabled && allowedCameras.includes(cam.name), + ) .map((cam) => cam.name) .join(","); } @@ -114,7 +118,7 @@ export default function LiveDashboardView({ .map((cam) => cam.name) .filter((cam) => config.camera_groups[cameraGroup]?.cameras.includes(cam)) .join(","); - }, [cameras, cameraGroup, config, includeBirdseye]); + }, [cameras, cameraGroup, config, includeBirdseye, allowedCameras]); const { data: allEvents, mutate: updateEvents } = useSWR([ "review", diff --git a/web/src/views/recording/RecordingView.tsx b/web/src/views/recording/RecordingView.tsx index 7f382dee1a..f19a86a17a 100644 --- a/web/src/views/recording/RecordingView.tsx +++ b/web/src/views/recording/RecordingView.tsx @@ -44,6 +44,7 @@ import { import { IoMdArrowRoundBack } from "react-icons/io"; import { useLocation, useNavigate } from "react-router-dom"; import { Toaster } from "@/components/ui/sonner"; +import { useIsAdmin } from "@/hooks/use-is-admin"; import useSWR from "swr"; import { TimeRange, TimelineType } from "@/types/timeline"; import MobileCameraDrawer from "@/components/overlay/MobileCameraDrawer"; @@ -109,6 +110,7 @@ export function RecordingView({ }: RecordingViewProps) { const { t } = useTranslation(["views/events", "components/dialog"]); const { data: config } = useSWR("config"); + const isAdmin = useIsAdmin(); const navigate = useNavigate(); const location = useLocation(); const contentRef = useRef(null); @@ -723,13 +725,17 @@ export function RecordingView({ setCustomShareTimestamp(initialTimestamp); setShareTimestampOpen(true); }} - onDebugReplayClick={() => { - setDebugReplayRange({ - after: timeRange.before - 60, - before: timeRange.before, - }); - setDebugReplayMode("select"); - }} + onDebugReplayClick={ + isAdmin + ? () => { + setDebugReplayRange({ + after: timeRange.before - 60, + before: timeRange.before, + }); + setDebugReplayMode("select"); + } + : undefined + } onExportClick={() => { const now = new Date(timeRange.before * 1000); now.setHours(now.getHours() - 1); From 7e0e0635b882e0303febd5d234dda6a917ac4809 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 24 May 2026 15:59:56 -0500 Subject: [PATCH 40/94] UI tweaks (#23304) * restructure camera enable/disable pane * remove obsolete camera edit form * change terminology to off/on instead of disabled/enabled * docs * move menu options and add current camera name badge * docs * tweaks --- docs/docs/configuration/cameras.md | 2 +- docs/docs/configuration/index.md | 6 +- docs/docs/configuration/live.md | 33 +- docs/docs/configuration/profiles.md | 10 +- docs/docs/configuration/reference.md | 4 +- docs/docs/guides/getting_started.md | 2 +- docs/docs/integrations/home-assistant.md | 10 +- docs/docs/integrations/mqtt.md | 6 +- web/public/locales/en/components/player.json | 2 +- web/public/locales/en/views/live.json | 6 +- web/public/locales/en/views/settings.json | 27 +- web/src/components/menu/LiveContextMenu.tsx | 2 +- web/src/components/player/LivePlayer.tsx | 2 +- .../components/settings/CameraEditForm.tsx | 755 ------------------ web/src/pages/Settings.tsx | 37 +- web/src/views/live/LiveCameraView.tsx | 4 +- .../views/settings/CameraManagementView.tsx | 594 +++++++------- 17 files changed, 419 insertions(+), 1083 deletions(-) delete mode 100644 web/src/components/settings/CameraEditForm.tsx diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index 8094c9f1c7..f21984c8b8 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -67,7 +67,7 @@ Additional cameras are simply added under the camera configuration section. -Navigate to and use the add camera button to configure each additional camera. +Navigate to and use the add camera button to configure each additional camera. diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 72f0cfd078..00a35a74ab 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -113,7 +113,7 @@ Here are some common starter configuration examples. These can be configured thr 3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` 4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` -6. Navigate to and add your camera with the appropriate RTSP stream URL +6. Navigate to and add your camera with the appropriate RTSP stream URL 7. Navigate to to add a motion mask for the camera timestamp @@ -192,7 +192,7 @@ cameras: 3. Navigate to and add a detector with **Type** `EdgeTPU` and **Device** `usb` 4. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 5. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` -6. Navigate to and add your camera with the appropriate RTSP stream URL +6. Navigate to and add your camera with the appropriate RTSP stream URL 7. Navigate to to add a motion mask for the camera timestamp @@ -270,7 +270,7 @@ cameras: 4. On the same page, in the **Custom Model** tab, configure the OpenVINO model path and settings 5. Navigate to and set **Enable recording** to on, **Motion retention > Retention days** to `7`, **Alert retention > Event retention > Retention days** to `30`, **Alert retention > Event retention > Retention mode** to `motion`, **Detection retention > Event retention > Retention days** to `30`, **Detection retention > Event retention > Retention mode** to `motion` 6. Navigate to and set **Enable snapshots** to on, **Snapshot retention > Default retention** to `30` -7. Navigate to and add your camera with the appropriate RTSP stream URL +7. Navigate to and add your camera with the appropriate RTSP stream URL 8. Navigate to to add a motion mask for the camera timestamp diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index 5749379c63..bc58d50bbc 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -257,19 +257,38 @@ cameras: -### Disabling cameras +### Camera state -Cameras can be temporarily disabled through the Frigate UI and through [MQTT](/integrations/mqtt#frigatecamera_nameenabledset) to conserve system resources. When disabled, Frigate's ffmpeg processes are terminated — recording stops, object detection is paused, and the Live dashboard displays a blank image with a disabled message. Review items, tracked objects, and historical footage for disabled cameras can still be accessed via the UI. +Each camera has three possible states, surfaced as a status selector in **Settings → Global configuration → Camera management**: -:::note +- **On** — streams are processed normally. Object detection, recording, and Live view are active. +- **Off** — Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. This state does **not** persist across Frigate restarts; the camera returns to On after a restart. +- **Disabled** — the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.** -Disabling a camera via the Frigate UI or MQTT is temporary and does not persist through restarts of Frigate. +#### Turning a camera on or off -::: +Turning a camera off is temporary and does not require a restart. The available controls are: -For restreamed cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source). +- The power button in the single-camera Live view header +- The right-click context menu on a camera tile on the Live dashboard +- The Camera management settings pane (status set to **Off**) +- The mobile settings drawer on the single-camera Live view (admin users only) +- The [MQTT topic](/integrations/mqtt#frigatecamera_nameenabledset) `frigate//enabled/set` with payload `ON` or `OFF` +- The Home Assistant integration via the [`camera.turn_on` / `camera.turn_off` actions](/integrations/home-assistant#camera-api) -Note that disabling a camera through the config file (`enabled: False`) removes all related UI elements, including historical footage access. To retain access while disabling the camera, keep it enabled in the config and use the UI or MQTT to disable it temporarily. +#### Disabling a camera + +Disabling a camera saves the change to your configuration file. Navigate to **Settings → Global configuration → Camera management** and set the camera's status to **Disabled**. Runtime processing stops immediately; the change persists across restarts. + +Re-enabling a disabled camera requires a restart of Frigate so that the ffmpeg processes and other camera-scoped resources can be initialized. The UI will prompt you to restart when you switch a disabled camera back to On. + +#### Restream behavior + +For both Off and Disabled cameras, go2rtc remains active but does not use system resources for decoding or processing unless there are active external consumers (such as the Advanced Camera Card in Home Assistant using a go2rtc source). + +#### Choosing Off versus Disabled + +If you want a camera's historical data (review items, tracked objects, footage) to stay accessible in the UI while you stop processing, set the camera to **Off**. If you want the camera fully removed from the Live dashboard, review filters, and other UI surfaces, set it to **Disabled**. The Disabled state still keeps the camera in Camera management so it can be re-enabled later; if you want to remove all traces of a camera including its configuration, delete it via Camera management instead. ### Live player error messages diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md index acb6cf4826..3954ee956f 100644 --- a/docs/docs/configuration/profiles.md +++ b/docs/docs/configuration/profiles.md @@ -33,10 +33,10 @@ The easiest way to define profiles is to use the Frigate UI. Profiles can also b -1. **Create a profile** — Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID). +1. **Create a profile** — Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID). 2. **Configure overrides** — Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides — fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides. -3. **Activate a profile** — Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers. -4. **Delete a profile** — Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it. +3. **Activate a profile** — Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers. +4. **Delete a profile** — Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it. @@ -135,10 +135,10 @@ A common use case is having different detection and notification settings based -1. Navigate to and create two profiles: **Home** and **Away**. +1. Navigate to and create two profiles: **Home** and **Away**. 2. From to the Camera configuration section in Settings, choose the **front_door** camera, and select the **Away** profile from the profile dropdown. Then, enable notifications from the Notifications pane, and set alert labels to `person` and `car` from the Review pane. Then, from the profile dropdown choose **Home** profile, then navigate to Notifications to disable notifications. 3. For the **indoor_cam** camera, perform similar steps - configure the **Away** profile to enable the camera, detection, and recording. Configure the **Home** profile to disable the camera entirely for privacy. -4. Activate the desired profile from or from the **Profiles** option in Frigate's main menu. +4. Activate the desired profile from or from the **Profiles** option in Frigate's main menu. diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md index e5eb161386..a006de8fd2 100644 --- a/docs/docs/configuration/reference.md +++ b/docs/docs/configuration/reference.md @@ -840,8 +840,8 @@ cameras: # Required: name of the camera back: # Optional: Enable/Disable the camera (default: shown below). - # If disabled: config is used but no live stream and no capture etc. - # Events/Recordings are still viewable. + # When False, ffmpeg is not started and the camera is hidden from the UI + # (except Camera Management). Re-enabling requires a Frigate restart. enabled: True # Optional: camera type used for some Frigate features (default: shown below) # Options are "generic" and "lpr" diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index f112a0de96..0c8d5d325a 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -144,7 +144,7 @@ At this point you should be able to start Frigate and a basic config will be cre ### Step 2: Add a camera -Click the **Add Camera** button in to use the camera setup wizard to get your first camera added into Frigate. +Click the **Add Camera** button in to use the camera setup wizard to get your first camera added into Frigate. ### Step 3: Configure hardware acceleration (recommended) diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md index 5b9c014377..14ee795289 100644 --- a/docs/docs/integrations/home-assistant.md +++ b/docs/docs/integrations/home-assistant.md @@ -195,7 +195,7 @@ For clips to be castable to media devices, audio is required and may need to be ## Camera API -To disable a camera dynamically +To turn a camera off (pauses Frigate's processing of the stream; does not persist across Frigate restarts; see [Camera state](/configuration/live#camera-state)): ``` action: camera.turn_off @@ -204,7 +204,7 @@ target: entity_id: camera.back_deck_cam # your Frigate camera entity ID ``` -To enable a camera that has been disabled dynamically +To turn a camera back on: ``` action: camera.turn_on @@ -213,6 +213,12 @@ target: entity_id: camera.back_deck_cam # your Frigate camera entity ID ``` +:::note + +These actions toggle Frigate's runtime On/Off state. To permanently disable a camera, set its status to **Disabled** in **Settings → Camera Management** in the Frigate UI. + +::: + ## Notification API Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 28c178d1f5..03f5135f4b 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -306,7 +306,7 @@ Publishes the current health status of each role that is enabled (`audio`, `dete - `online`: Stream is running and being processed - `offline`: Stream is offline and is being restarted -- `disabled`: Camera is currently disabled +- `disabled`: Camera is currently turned off (either at runtime via the `enabled/set` topic, or persistently via the configuration file). See [Camera state](/configuration/live#camera-state) for the distinction. ### `frigate//` @@ -368,11 +368,11 @@ The published value is the detected state class name (e.g., `open`, `closed`, `o ### `frigate//enabled/set` -Topic to turn Frigate's processing of a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn Frigate's processing of a camera on or off at runtime. Expected values are `ON` and `OFF`. The change is **not** persisted across Frigate restarts — the camera returns to the configured state on restart. To permanently disable a camera, use **Settings → Global configuration → Camera management** in the Frigate UI. See [Camera state](/configuration/live#camera-state) for the difference between turning a camera off and disabling it. ### `frigate//enabled/state` -Topic with current state of processing for a camera. Published values are `ON` and `OFF`. +Topic with current runtime state of processing for a camera. Published values are `ON` and `OFF`. ### `frigate//detect/set` diff --git a/web/public/locales/en/components/player.json b/web/public/locales/en/components/player.json index 6ceef7e0cd..011baaa1bf 100644 --- a/web/public/locales/en/components/player.json +++ b/web/public/locales/en/components/player.json @@ -12,7 +12,7 @@ "title": "Stream Offline", "desc": "No frames have been received on the {{cameraName}} detect stream, check error logs" }, - "cameraDisabled": "Camera is disabled", + "cameraOff": "Camera is off", "stats": { "streamType": { "title": "Stream Type:", diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json index 3aa892222a..f2ea2721f1 100644 --- a/web/public/locales/en/views/live.json +++ b/web/public/locales/en/views/live.json @@ -57,8 +57,8 @@ "presets": "PTZ camera presets" }, "camera": { - "enable": "Enable Camera", - "disable": "Disable Camera" + "turnOn": "Turn Camera On", + "turnOff": "Turn Camera Off" }, "muteCameras": { "enable": "Mute All Cameras", @@ -153,7 +153,7 @@ }, "cameraSettings": { "title": "{{camera}} Settings", - "cameraEnabled": "Camera Enabled", + "camera": "Camera", "objectDetection": "Object Detection", "recording": "Recording", "snapshots": "Snapshots", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 11fcd92123..9731eb22dc 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -103,7 +103,7 @@ "cameraUi": "Camera UI", "cameraTimestampStyle": "Timestamp style", "cameraMqtt": "Camera MQTT", - "cameraManagement": "Management", + "cameraManagement": "Camera management", "cameraReview": "Review", "masksAndZones": "Masks / Zones", "motionTuner": "Motion tuner", @@ -457,7 +457,7 @@ }, "cameraManagement": { "title": "Manage Cameras", - "description": "Add, edit, and delete cameras, control which cameras are enabled, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.", + "description": "Add, edit, and delete cameras, control the state of each camera, and configure per-profile and camera type overrides. To configure streams, detection, motion, and other camera-specific settings, choose the specific section under Camera Configuration.", "addCamera": "Add New Camera", "deleteCamera": "Delete Camera", "deleteCameraDialog": { @@ -475,12 +475,17 @@ "selectCamera": "Select a Camera", "backToSettings": "Back to Camera Settings", "streams": { - "title": "Enable / Disable Cameras", - "enableLabel": "Enabled cameras", - "enableDesc": "Temporarily disable an enabled camera until Frigate restarts. Disabling a camera completely stops Frigate's processing of this camera's streams. Detection, recording, and debugging will be unavailable.
    Note: This does not disable go2rtc restreams.

    Drag the handle to reorder the cameras as they appear in the UI. The order of enabled cameras will be reflected throughout the UI including the Live dashboard and camera selection dropdowns.", - "disableLabel": "Disabled cameras", - "disableDesc": "Enable a camera that is currently not visible in the UI and disabled in the configuration. A restart of Frigate is required after enabling.", - "enableSuccess": "Enabled {{cameraName}} in configuration. Restart Frigate to apply the changes.", + "title": "Camera State and Details", + "label": "Camera state", + "description": "Set the operating state for each camera.

    On: streams are processed normally.
    Off: temporarily pauses processing. Does not persist across Frigate restarts.
    Disabled: stops processing and saves the change to your configuration. A restart is required to re-enable a disabled camera.

    Note: Disabling does not affect go2rtc restreams.

    Drag the handle to reorder active cameras as they appear throughout the UI, including the Live dashboard and camera selection dropdowns.", + "disabledSubheading": "Disabled in configuration", + "status": { + "on": "On", + "off": "Off", + "disabled": "Disabled" + }, + "enableSuccess": "Enabled {{cameraName}}. Restart Frigate to apply.", + "disableSuccess": "Disabled {{cameraName}} and saved to configuration.", "reorderHandle": "Drag to reorder", "saving": "Saving…", "saved": "Saved", @@ -527,10 +532,10 @@ "profiles": { "title": "Profile Camera Overrides", "selectLabel": "Select profile", - "description": "Configure which cameras are enabled or disabled when a profile is activated. Cameras set to \"Inherit\" keep their base enabled state.", + "description": "Configure which cameras are turned on or off when a profile is activated. Cameras set to \"Inherit\" keep their default state.", "inherit": "Inherit", - "enabled": "Enabled", - "disabled": "Disabled" + "on": "On", + "off": "Off" }, "cameraType": { "title": "Camera Type", diff --git a/web/src/components/menu/LiveContextMenu.tsx b/web/src/components/menu/LiveContextMenu.tsx index 8ed78e348c..c5c8c56c9a 100644 --- a/web/src/components/menu/LiveContextMenu.tsx +++ b/web/src/components/menu/LiveContextMenu.tsx @@ -320,7 +320,7 @@ export default function LiveContextMenu({ onClick={() => sendEnabled(isEnabled ? "OFF" : "ON")} >
    - {isEnabled ? t("camera.disable") : t("camera.enable")} + {isEnabled ? t("camera.turnOff") : t("camera.turnOn")}
    diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 0ab7033528..0170f9dd1e 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -488,7 +488,7 @@ export default function LivePlayer({

    - {t("cameraDisabled")} + {t("cameraOff")}

    diff --git a/web/src/components/settings/CameraEditForm.tsx b/web/src/components/settings/CameraEditForm.tsx deleted file mode 100644 index efae88aac5..0000000000 --- a/web/src/components/settings/CameraEditForm.tsx +++ /dev/null @@ -1,755 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { Switch } from "@/components/ui/switch"; -import { Card, CardContent } from "@/components/ui/card"; -import Heading from "@/components/ui/heading"; -import { Separator } from "@/components/ui/separator"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm, useFieldArray } from "react-hook-form"; -import { z } from "zod"; -import axios from "axios"; -import { toast } from "sonner"; -import { useTranslation } from "react-i18next"; -import { useState, useMemo, useEffect } from "react"; -import { LuTrash2, LuPlus } from "react-icons/lu"; -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { FrigateConfig } from "@/types/frigateConfig"; -import useSWR from "swr"; -import { processCameraName } from "@/utils/cameraUtil"; -import { Label } from "@/components/ui/label"; -import { ConfigSetBody } from "@/types/cameraWizard"; -import { Toaster } from "../ui/sonner"; - -const RoleEnum = z.enum(["audio", "detect", "record"]); -type Role = z.infer; - -type CameraEditFormProps = { - cameraName?: string; - onSave?: () => void; - onCancel?: () => void; -}; - -export default function CameraEditForm({ - cameraName, - onSave, - onCancel, -}: CameraEditFormProps) { - const { t } = useTranslation(["views/settings"]); - const { data: config, mutate: mutateConfig } = - useSWR("config"); - const { data: rawPaths, mutate: mutateRawPaths } = useSWR<{ - cameras: Record< - string, - { ffmpeg: { inputs: { path: string; roles: string[] }[] } } - >; - go2rtc: { streams: Record }; - }>(cameraName ? "config/raw_paths" : null); - const [isLoading, setIsLoading] = useState(false); - - const formSchema = useMemo( - () => - z.object({ - cameraName: z - .string() - .min(1, { message: t("cameraManagement.cameraConfig.nameRequired") }), - enabled: z.boolean(), - ffmpeg: z.object({ - inputs: z - .array( - z.object({ - path: z.string().min(1, { - message: t( - "cameraManagement.cameraConfig.ffmpeg.pathRequired", - ), - }), - roles: z.array(RoleEnum).min(1, { - message: t( - "cameraManagement.cameraConfig.ffmpeg.rolesRequired", - ), - }), - }), - ) - .min(1, { - message: t("cameraManagement.cameraConfig.ffmpeg.inputsRequired"), - }) - .refine( - (inputs) => { - const roleOccurrences = new Map(); - inputs.forEach((input) => { - input.roles.forEach((role) => { - roleOccurrences.set( - role, - (roleOccurrences.get(role) || 0) + 1, - ); - }); - }); - return Array.from(roleOccurrences.values()).every( - (count) => count <= 1, - ); - }, - { - message: t("cameraManagement.cameraConfig.ffmpeg.rolesUnique"), - path: ["inputs"], - }, - ), - }), - go2rtcStreams: z.record(z.string(), z.array(z.string())).optional(), - }), - [t], - ); - - type FormValues = z.infer; - - const cameraInfo = useMemo(() => { - if (!cameraName || !config?.cameras[cameraName]) { - return { - friendly_name: undefined, - name: cameraName || "", - roles: new Set(), - go2rtcStreams: {}, - }; - } - - const camera = config.cameras[cameraName]; - const roles = new Set(); - - camera.ffmpeg?.inputs?.forEach((input) => { - input.roles.forEach((role) => roles.add(role as Role)); - }); - - // Load existing go2rtc streams - const go2rtcStreams = config.go2rtc?.streams || {}; - - return { - friendly_name: camera?.friendly_name || cameraName, - name: cameraName, - roles, - go2rtcStreams, - }; - }, [cameraName, config]); - - const defaultValues: FormValues = { - cameraName: cameraInfo?.friendly_name || cameraName || "", - enabled: true, - ffmpeg: { - inputs: [ - { - path: "", - roles: cameraInfo.roles.has("detect") ? [] : ["detect"], - }, - ], - }, - go2rtcStreams: {}, - }; - - // Load existing camera config if editing - if (cameraName && config?.cameras[cameraName]) { - const camera = config.cameras[cameraName]; - defaultValues.enabled = camera.enabled ?? true; - - // Use raw paths from the admin endpoint if available, otherwise fall back to masked paths - const rawCameraData = rawPaths?.cameras?.[cameraName]; - defaultValues.ffmpeg.inputs = rawCameraData?.ffmpeg?.inputs?.length - ? rawCameraData.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })) - : camera.ffmpeg?.inputs?.length - ? camera.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })) - : defaultValues.ffmpeg.inputs; - - const go2rtcStreams = - rawPaths?.go2rtc?.streams || config.go2rtc?.streams || {}; - const cameraStreams: Record = {}; - - // get candidate stream names for this camera. could be the camera's own name, - // any restream names referenced by this camera, or any keys under live --> streams - const validNames = new Set(); - validNames.add(cameraName); - - // deduce go2rtc stream names from rtsp restream inputs - camera.ffmpeg?.inputs?.forEach((input) => { - // exclude any query strings or trailing slashes from the stream name - const restreamMatch = input.path.match( - /^rtsp:\/\/127\.0\.0\.1:8554\/([^?#/]+)(?:[?#].*)?$/, - ); - if (restreamMatch) { - const streamName = restreamMatch[1]; - validNames.add(streamName); - } - }); - - // Include live --> streams keys - const liveStreams = camera?.live?.streams; - if (liveStreams) { - Object.keys(liveStreams).forEach((key) => { - validNames.add(key); - }); - } - - // Map only go2rtc entries that match the collected names - Object.entries(go2rtcStreams).forEach(([name, urls]) => { - if (validNames.has(name)) { - cameraStreams[name] = Array.isArray(urls) ? urls : [urls]; - } - }); - - defaultValues.go2rtcStreams = cameraStreams; - } - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues, - mode: "onChange", - }); - - // Update form values when rawPaths loads - useEffect(() => { - if ( - cameraName && - config?.cameras[cameraName] && - rawPaths?.cameras?.[cameraName] - ) { - const camera = config.cameras[cameraName]; - const rawCameraData = rawPaths.cameras[cameraName]; - - // Update ffmpeg inputs with raw paths - if (rawCameraData.ffmpeg?.inputs?.length) { - form.setValue( - "ffmpeg.inputs", - rawCameraData.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles as Role[], - })), - ); - } - - // Update go2rtc streams with raw URLs - if (rawPaths.go2rtc?.streams) { - const validNames = new Set(); - validNames.add(cameraName); - - camera.ffmpeg?.inputs?.forEach((input) => { - const restreamMatch = input.path.match( - /^rtsp:\/\/127\.0\.0\.1:8554\/([^?#/]+)(?:[?#].*)?$/, - ); - if (restreamMatch) { - validNames.add(restreamMatch[1]); - } - }); - - const liveStreams = camera?.live?.streams; - if (liveStreams) { - Object.keys(liveStreams).forEach((key) => validNames.add(key)); - } - - const cameraStreams: Record = {}; - Object.entries(rawPaths.go2rtc.streams).forEach(([name, urls]) => { - if (validNames.has(name)) { - cameraStreams[name] = Array.isArray(urls) ? urls : [urls]; - } - }); - - if (Object.keys(cameraStreams).length > 0) { - form.setValue("go2rtcStreams", cameraStreams); - } - } - } - }, [cameraName, config, rawPaths, form]); - - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: "ffmpeg.inputs", - }); - - // Watch ffmpeg.inputs to track used roles - const watchedInputs = form.watch("ffmpeg.inputs"); - - // Watch go2rtc streams - const watchedGo2rtcStreams = form.watch("go2rtcStreams") || {}; - - const saveCameraConfig = (values: FormValues) => { - setIsLoading(true); - const { finalCameraName, friendlyName } = processCameraName( - values.cameraName, - ); - - const configData: ConfigSetBody["config_data"] = { - cameras: { - [finalCameraName]: { - enabled: values.enabled, - ...(friendlyName && { friendly_name: friendlyName }), - ffmpeg: { - inputs: values.ffmpeg.inputs.map((input) => ({ - path: input.path, - roles: input.roles, - })), - }, - }, - }, - }; - - // Add go2rtc streams if provided - if (values.go2rtcStreams && Object.keys(values.go2rtcStreams).length > 0) { - configData.go2rtc = { - streams: values.go2rtcStreams, - }; - } - - const requestBody: ConfigSetBody = { - requires_restart: 1, - config_data: configData, - }; - - // Add update_topic for new cameras - if (!cameraName) { - requestBody.update_topic = `config/cameras/${finalCameraName}/add`; - } - - axios - .put("config/set", requestBody) - .then((res) => { - if (res.status === 200) { - // Update running go2rtc instance if streams were configured - if ( - values.go2rtcStreams && - Object.keys(values.go2rtcStreams).length > 0 - ) { - const updatePromises = Object.entries(values.go2rtcStreams).map( - ([streamName, urls]) => - axios.put( - `go2rtc/streams/${streamName}?src=${encodeURIComponent(urls[0])}`, - ), - ); - - Promise.allSettled(updatePromises).then(() => { - toast.success( - t("cameraManagement.cameraConfig.toast.success", { - cameraName: values.cameraName, - }), - { position: "top-center" }, - ); - mutateConfig(); - mutateRawPaths(); - if (onSave) onSave(); - }); - } else { - toast.success( - t("cameraManagement.cameraConfig.toast.success", { - cameraName: values.cameraName, - }), - { position: "top-center" }, - ); - mutateConfig(); - mutateRawPaths(); - if (onSave) onSave(); - } - } else { - throw new Error(res.statusText); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), - { position: "top-center" }, - ); - }) - .finally(() => { - setIsLoading(false); - }); - }; - - const onSubmit = (values: FormValues) => { - if ( - cameraName && - values.cameraName !== cameraName && - values.cameraName !== cameraInfo?.friendly_name - ) { - // If camera name changed, delete old camera config - const deleteRequestBody = { - requires_restart: 1, - config_data: { - cameras: { - [cameraName]: null, - }, - }, - update_topic: `config/cameras/${cameraName}/remove`, - }; - - axios - .put("config/set", deleteRequestBody) - .then(() => saveCameraConfig(values)) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), - { position: "top-center" }, - ); - }) - .finally(() => { - setIsLoading(false); - }); - } else { - saveCameraConfig(values); - } - }; - - // Determine available roles for new streams - const getAvailableRoles = (): Role[] => { - const used = new Set(); - watchedInputs.forEach((input) => { - input.roles.forEach((role) => used.add(role)); - }); - return used.has("detect") ? [] : ["detect"]; - }; - - const getUsedRolesExcludingIndex = (excludeIndex: number) => { - const roles = new Set(); - watchedInputs.forEach((input, idx) => { - if (idx !== excludeIndex) { - input.roles.forEach((role) => roles.add(role)); - } - }); - return roles; - }; - - return ( -
    - - - {cameraName - ? t("cameraManagement.cameraConfig.edit") - : t("cameraManagement.cameraConfig.add")} - -
    - {t("cameraManagement.cameraConfig.description")} -
    - - -
    - - ( - - {t("cameraManagement.cameraConfig.name")} - - - - - - )} - /> - - ( - - - - - - {t("cameraManagement.cameraConfig.enabled")} - - - - )} - /> - -
    - - {fields.map((field, index) => ( - - -
    -

    - {t("cameraWizard.step3.streamTitle", { - number: index + 1, - })} -

    - -
    - - ( - - - {t("cameraManagement.cameraConfig.ffmpeg.path")} - - - - - - - )} - /> - -
    - -
    -
    - {(["detect", "record", "audio"] as const).map( - (role) => { - const isUsedElsewhere = - getUsedRolesExcludingIndex(index).has(role); - const isChecked = - watchedInputs[index]?.roles?.includes(role) || - false; - return ( -
    - - {role} - - { - const currentRoles = - watchedInputs[index]?.roles || []; - const updatedRoles = checked - ? [...currentRoles, role] - : currentRoles.filter((r) => r !== role); - form.setValue( - `ffmpeg.inputs.${index}.roles`, - updatedRoles, - ); - }} - disabled={!isChecked && isUsedElsewhere} - /> -
    - ); - }, - )} -
    -
    -
    -
    -
    - ))} - - {form.formState.errors.ffmpeg?.inputs?.root && - form.formState.errors.ffmpeg.inputs.root.message} - - -
    - - {/* go2rtc Streams Section */} - {Object.keys(watchedGo2rtcStreams).length > 0 && ( -
    - - {Object.entries(watchedGo2rtcStreams).map( - ([streamName, urls]) => ( - - -
    -

    {streamName}

    - -
    - -
    - - {(Array.isArray(urls) ? urls : [urls]).map( - (url, urlIndex) => ( -
    - { - const updatedStreams = { - ...watchedGo2rtcStreams, - }; - const currentUrls = Array.isArray( - updatedStreams[streamName], - ) - ? updatedStreams[streamName] - : [updatedStreams[streamName]]; - currentUrls[urlIndex] = e.target.value; - updatedStreams[streamName] = currentUrls; - form.setValue( - "go2rtcStreams", - updatedStreams, - ); - }} - placeholder="rtsp://username:password@host:port/path" - /> - {(Array.isArray(urls) ? urls : [urls]).length > - 1 && ( - - )} -
    - ), - )} - -
    -
    -
    - ), - )} - -
    - )} - -
    - - -
    - - -
    - ); -} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index be4a036c0b..8f02826285 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -15,6 +15,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; +import { Badge } from "@/components/ui/badge"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Collapsible, @@ -310,6 +311,8 @@ const settingsGroups = [ { label: "globalConfig", items: [ + { key: "profiles", component: ProfilesView }, + { key: "cameraManagement", component: CameraManagementView }, { key: "globalDetect", component: GlobalDetectSettingsPage }, { key: "globalObjects", component: GlobalObjectsSettingsPage }, { key: "globalMotion", component: GlobalMotionSettingsPage }, @@ -331,8 +334,6 @@ const settingsGroups = [ { label: "cameras", items: [ - { key: "profiles", component: ProfilesView }, - { key: "cameraManagement", component: CameraManagementView }, { key: "cameraDetect", component: CameraDetectSettingsPage }, { key: "cameraObjects", component: CameraObjectsSettingsPage }, { key: "cameraMotion", component: CameraMotionSettingsPage }, @@ -1651,6 +1652,8 @@ export default function Settings() { const isMultiItem = filteredItems.length > 1; const renderedExpanded = !isMultiItem || !collapsedGroups.has(group.label); + const showCameraBadge = + group.label === "cameras" && !!selectedCamera; const items = filteredItems.map((item) => ( - {items} + + {showCameraBadge && ( +
    + + + +
    + )} + {items} +
    ) : ( items @@ -2027,6 +2042,22 @@ export default function Settings() { + {group.label === "cameras" && + selectedCamera && ( + + )} {filteredItems.map((item) => ( sendEnabled(enabledState == "ON" ? "OFF" : "ON")} disabled={debug} @@ -1489,7 +1489,7 @@ function FrigateCameraFeatures({ {isAdmin && ( <> sendEnabled(enabledState == "ON" ? "OFF" : "ON") diff --git a/web/src/views/settings/CameraManagementView.tsx b/web/src/views/settings/CameraManagementView.tsx index 212b32389a..f18b8f94b4 100644 --- a/web/src/views/settings/CameraManagementView.tsx +++ b/web/src/views/settings/CameraManagementView.tsx @@ -11,7 +11,6 @@ import { Button } from "@/components/ui/button"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { useTranslation } from "react-i18next"; -import CameraEditForm from "@/components/settings/CameraEditForm"; import CameraWizardDialog from "@/components/settings/CameraWizardDialog"; import DeleteCameraDialog from "@/components/overlay/dialog/DeleteCameraDialog"; import { @@ -20,15 +19,13 @@ import { LuGripVertical, LuPencil, LuPlus, + LuRefreshCcw, LuTrash2, } from "react-icons/lu"; import { Reorder, useDragControls } from "framer-motion"; -import { IoMdArrowRoundBack } from "react-icons/io"; import { Link } from "react-router-dom"; import { useDocDomain } from "@/hooks/use-doc-domain"; -import { isDesktop } from "react-device-detect"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; -import { Switch } from "@/components/ui/switch"; import { Trans } from "react-i18next"; import { useEnabledState, useRestart } from "@/api/ws"; import { Label } from "@/components/ui/label"; @@ -78,12 +75,10 @@ const REORDER_SAVED_INDICATOR_MS = 1500; type ReorderSaveStatus = "idle" | "saving" | "saved"; type CameraManagementViewProps = { - setUnsavedChanges: React.Dispatch>; profileState?: ProfileState; }; export default function CameraManagementView({ - setUnsavedChanges, profileState, }: CameraManagementViewProps) { const { t } = useTranslation(["views/settings", "common"]); @@ -91,12 +86,6 @@ export default function CameraManagementView({ const { data: config, mutate: updateConfig } = useSWR("config"); - const [viewMode, setViewMode] = useState<"settings" | "add" | "edit">( - "settings", - ); // Control view state - const [editCameraName, setEditCameraName] = useState( - undefined, - ); // Track camera being edited const [showWizard, setShowWizard] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); @@ -226,14 +215,6 @@ export default function CameraManagementView({ document.title = t("documentTitle.cameraManagement"); }, [t]); - // Handle back navigation from add/edit form - const handleBack = useCallback(() => { - setViewMode("settings"); - setEditCameraName(undefined); - setUnsavedChanges(false); - updateConfig(); - }, [updateConfig, setUnsavedChanges]); - return ( <>
    - {viewMode === "settings" ? ( - <> - - {t("cameraManagement.title")} - -

    - {t("cameraManagement.description")} -

    + + {t("cameraManagement.title")} + +

    + {t("cameraManagement.description")} +

    -
    -
    - - {enabledCameras.length + disabledCameras.length > 0 && ( - - )} -
    +
    +
    + + {enabledCameras.length + disabledCameras.length > 0 && ( + + )} +
    - {enabledCameras.length > 0 && ( - 0 || disabledCameras.length > 0) && ( + + cameraManagement.streams.title + + } + > +
    +
    + +

    - cameraManagement.streams.title + cameraManagement.streams.description - } - > -

    -
    - -
    -
    +

    +
    +
    +
    + {orderedCameras.length > 0 && ( {orderedCameras.map((camera) => ( - ))} - -
    -

    - - cameraManagement.streams.enableDesc - -

    -
    - {disabledCameras.length > 0 && ( -
    -
    - -

    - {t("cameraManagement.streams.disableDesc")} + )} + {orderedCameras.length > 0 && + disabledCameras.length > 0 && ( +

    + )} + {disabledCameras.length > 0 && ( +
    +

    + {t("cameraManagement.streams.disabledSubheading")}

    + {disabledCameras.map((camera) => ( + + ))}
    -
    -
    - {disabledCameras.map((camera) => ( -
    - - -
    - ))} -
    -

    - {t("cameraManagement.streams.disableDesc")} -

    -
    -
    - )} - - )} - - {profileState && - profileState.allProfileNames.length > 0 && - enabledCameras.length > 0 && ( - - )} - - {config?.lpr?.enabled && allCameras.length > 0 && ( - - )} -
    - - ) : ( - <> -
    - -
    -
    - +
    +

    + + cameraManagement.streams.description + +

    +
    + + )} + + {profileState && + profileState.allProfileNames.length > 0 && + enabledCameras.length > 0 && ( + -
    - - )} + )} + + {config?.lpr?.enabled && allCameras.length > 0 && ( + + )} +
    @@ -468,17 +399,19 @@ function ReorderSaveStatusIndicator({ ); } -type EnabledCameraRowProps = { +type ActiveCameraRowProps = { camera: string; onConfigChanged: () => Promise; onDragEnd: () => void; + setRestartDialogOpen: React.Dispatch>; }; -function EnabledCameraRow({ +function ActiveCameraRow({ camera, onConfigChanged, onDragEnd, -}: EnabledCameraRowProps) { + setRestartDialogOpen, +}: ActiveCameraRowProps) { const { t } = useTranslation(["views/settings"]); const controls = useDragControls(); @@ -506,38 +439,226 @@ function EnabledCameraRow({ onConfigChanged={onConfigChanged} />
    - + ); } -type CameraEnableSwitchProps = { - cameraName: string; +type DisabledCameraRowProps = { + camera: string; + onConfigChanged: () => Promise; + setRestartDialogOpen: React.Dispatch>; }; -function CameraEnableSwitch({ cameraName }: CameraEnableSwitchProps) { - const { payload: enabledState, send: sendEnabled } = - useEnabledState(cameraName); - const { data: config } = useSWR("config"); - - const isChecked = - enabledState === "ON" || enabledState === "OFF" - ? enabledState === "ON" - : (config?.cameras?.[cameraName]?.enabled ?? false); - +function DisabledCameraRow({ + camera, + onConfigChanged, + setRestartDialogOpen, +}: DisabledCameraRowProps) { return ( -
    - { - sendEnabled(isChecked ? "ON" : "OFF"); - }} +
    +
    + + +
    +
    ); } +type CameraStatus = "on" | "off" | "disabled"; + +type CameraStatusSelectProps = { + cameraName: string; + isDisabledInConfig: boolean; + onConfigChanged: () => Promise; + setRestartDialogOpen: React.Dispatch>; +}; + +function CameraStatusSelect({ + cameraName, + isDisabledInConfig, + onConfigChanged, + setRestartDialogOpen, +}: CameraStatusSelectProps) { + const { t } = useTranslation([ + "views/settings", + "components/dialog", + "common", + ]); + const { payload: enabledState, send: sendEnabled } = + useEnabledState(cameraName); + const [isSaving, setIsSaving] = useState(false); + + const currentStatus: CameraStatus = isDisabledInConfig + ? "disabled" + : enabledState === "OFF" + ? "off" + : "on"; + + const restartLabel = t("configForm.restartRequiredField", { + ns: "views/settings", + defaultValue: "Restart required", + }); + + const handleChange = useCallback( + async (newStatus: string) => { + if (newStatus === currentStatus || isSaving) { + return; + } + + if (newStatus === "on" && !isDisabledInConfig) { + sendEnabled("ON"); + return; + } + + if (newStatus === "off" && !isDisabledInConfig) { + sendEnabled("OFF"); + return; + } + + if (newStatus === "on" && isDisabledInConfig) { + setIsSaving(true); + try { + await axios.put("config/set", { + requires_restart: 1, + config_data: { + cameras: { [cameraName]: { enabled: true } }, + }, + }); + await onConfigChanged(); + toast.success( + t("cameraManagement.streams.enableSuccess", { + ns: "views/settings", + cameraName, + }), + { + position: "top-center", + action: ( + setRestartDialogOpen(true)}> + + + ), + }, + ); + } catch (error) { + const errorMessage = + axios.isAxiosError(error) && + (error.response?.data?.message || error.response?.data?.detail) + ? error.response?.data?.message || error.response?.data?.detail + : t("toast.save.error.noMessage", { ns: "common" }); + toast.error( + t("toast.save.error.title", { errorMessage, ns: "common" }), + { position: "top-center" }, + ); + } finally { + setIsSaving(false); + } + return; + } + + if (newStatus === "disabled" && !isDisabledInConfig) { + setIsSaving(true); + try { + // Stop runtime processing immediately before persisting the + // disable so the camera stops working without waiting for + // a restart. The config write below makes the change durable. + sendEnabled("OFF"); + await axios.put("config/set", { + requires_restart: 0, + config_data: { + cameras: { [cameraName]: { enabled: false } }, + }, + }); + await onConfigChanged(); + toast.success( + t("cameraManagement.streams.disableSuccess", { + ns: "views/settings", + cameraName, + }), + { position: "top-center" }, + ); + } catch (error) { + const errorMessage = + axios.isAxiosError(error) && + (error.response?.data?.message || error.response?.data?.detail) + ? error.response?.data?.message || error.response?.data?.detail + : t("toast.save.error.noMessage", { ns: "common" }); + toast.error( + t("toast.save.error.title", { errorMessage, ns: "common" }), + { position: "top-center" }, + ); + } finally { + setIsSaving(false); + } + return; + } + }, + [ + cameraName, + currentStatus, + isDisabledInConfig, + isSaving, + onConfigChanged, + sendEnabled, + setRestartDialogOpen, + t, + ], + ); + + if (isSaving) { + return ( +
    + +
    + ); + } + + return ( + + ); +} + type CameraDetailsEditorProps = { cameraName: string; onConfigChanged: () => Promise; @@ -783,97 +904,6 @@ function CameraDetailsEditor({ ); } -type CameraConfigEnableSwitchProps = { - cameraName: string; - setRestartDialogOpen: React.Dispatch>; - onConfigChanged: () => Promise; -}; - -function CameraConfigEnableSwitch({ - cameraName, - onConfigChanged, - setRestartDialogOpen, -}: CameraConfigEnableSwitchProps) { - const { t } = useTranslation([ - "common", - "views/settings", - "components/dialog", - ]); - const [isSaving, setIsSaving] = useState(false); - - const onCheckedChange = useCallback( - async (isChecked: boolean) => { - if (!isChecked || isSaving) { - return; - } - - setIsSaving(true); - - try { - await axios.put("config/set", { - requires_restart: 1, - config_data: { - cameras: { - [cameraName]: { - enabled: true, - }, - }, - }, - }); - - await onConfigChanged(); - - toast.success( - t("cameraManagement.streams.enableSuccess", { - ns: "views/settings", - cameraName, - }), - { - position: "top-center", - action: ( - setRestartDialogOpen(true)}> - - - ), - }, - ); - } catch (error) { - const errorMessage = - axios.isAxiosError(error) && - (error.response?.data?.message || error.response?.data?.detail) - ? error.response?.data?.message || error.response?.data?.detail - : t("toast.save.error.noMessage", { ns: "common" }); - - toast.error( - t("toast.save.error.title", { errorMessage, ns: "common" }), - { - position: "top-center", - }, - ); - } finally { - setIsSaving(false); - } - }, - [cameraName, isSaving, onConfigChanged, setRestartDialogOpen, t], - ); - - return ( -
    - {isSaving ? ( - - ) : ( - - )} -
    - ); -} - type CameraTypeSectionProps = { cameras: string[]; config: FrigateConfig | undefined; @@ -1231,12 +1261,12 @@ function ProfileCameraEnableSection({ })} - {t("cameraManagement.profiles.enabled", { + {t("cameraManagement.profiles.on", { ns: "views/settings", })} - {t("cameraManagement.profiles.disabled", { + {t("cameraManagement.profiles.off", { ns: "views/settings", })} From 90248ef2434ace220bbbab67112237e99d2c641c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 25 May 2026 08:02:57 -0500 Subject: [PATCH 41/94] remove camera name badge (#23305) --- web/src/pages/Settings.tsx | 73 ++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 8f02826285..d46b372abd 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -15,7 +15,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Badge } from "@/components/ui/badge"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { Collapsible, @@ -1652,8 +1651,6 @@ export default function Settings() { const isMultiItem = filteredItems.length > 1; const renderedExpanded = !isMultiItem || !collapsedGroups.has(group.label); - const showCameraBadge = - group.label === "cameras" && !!selectedCamera; const items = filteredItems.map((item) => ( toggleGroupCollapsed(group.label)} > -
    {t("menu." + group.label)}
    +
    + {t("menu." + group.label)} + {group.label === "cameras" && + renderedExpanded && + selectedCamera && ( +
    + +
    + )} +
    - - {showCameraBadge && ( -
    - - - -
    - )} - {items} -
    + {items} ) : ( items @@ -2030,8 +2024,33 @@ export default function Settings() { : "text-sidebar-foreground/80", )} > - -
    {t("menu." + group.label)}
    + +
    + {t("menu." + group.label)} + {group.label === "cameras" && + renderedExpanded && + selectedCamera && ( +
    + +
    + )} +
    - {group.label === "cameras" && - selectedCamera && ( - - )} {filteredItems.map((item) => ( Date: Mon, 25 May 2026 08:04:00 -0500 Subject: [PATCH 42/94] Profiles fixes (#23306) * add prop to disable id field * disable id field when editing profile mask/zone also, disable if the zone name already exists in required_zones or the base config is being edited and the id already exists on a profile * add backend validation to reject profile-omly masks/zones * add tests * update docs * tweak --- docs/docs/configuration/profiles.md | 28 +++- frigate/config/config.py | 42 ++++++ frigate/test/test_profiles.py | 135 ++++++++++++++++++ web/src/components/input/NameAndIdFields.tsx | 16 ++- .../settings/MotionMaskEditPane.tsx | 6 +- .../settings/ObjectMaskEditPane.tsx | 6 +- web/src/components/settings/ZoneEditPane.tsx | 30 +++- 7 files changed, 255 insertions(+), 8 deletions(-) diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md index 3954ee956f..a7b3b2f50d 100644 --- a/docs/docs/configuration/profiles.md +++ b/docs/docs/configuration/profiles.md @@ -126,7 +126,9 @@ Only the fields you explicitly set in a profile override are applied. All other ## Activating Profiles -Profiles can be activated and deactivated from the Frigate UI. Open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect. +Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integrations/mqtt#frigateprofileset), or the Home Assistant integration. + +In the Frigate UI, open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect. ## Example: Home / Away Setup @@ -207,3 +209,27 @@ In this example: - **Away profile**: The front door camera enables notifications and tracks specific alert labels. The indoor camera is fully enabled with detection and recording. - **Home profile**: The front door camera disables notifications. The indoor camera is completely disabled for privacy. - **No profile active**: All cameras use their base configuration values. + +## FAQ + +### Can I define a zone or mask in a profile but not have it in the base config? + +No. Profiles are pure overrides. Every zone and mask defined under a profile must reference an entry that already exists on the base camera config. Configurations that introduce profile-only zones or masks are rejected at startup. + +If you want a zone or mask to be active only under a specific profile, define it on the base config with `enabled: false`, then enable it in that profile's overrides. + +### How do I revert a profile zone or mask override back to the base configuration? + +Delete the override. In the Frigate UI, edit the profile and use the "Revert override" action (the trash can icon) on the zone or mask. The base entry is left untouched, and once the override is removed the profile inherits the base values for that zone or mask. + +### Can multiple profiles be active at the same time? + +No. Only one profile can be active at a time. Activating a new profile automatically deactivates the current one. + +### What happens to my profile overrides if I delete a zone or mask from the base? + +When you delete a base zone or mask in the Frigate UI, any profile overrides for that entry are deleted automatically as part of the same operation. If you remove a base entry by editing your config file directly and leave a profile override behind, the config will fail validation at startup until the orphaned override is removed as well. + +### Why are some settings missing when I configure a profile override? + +Fields that require a Frigate restart to take effect cannot be overridden by profiles, since profiles are applied at runtime without restarting. Those fields are hidden when editing a profile override and can only be changed on the base configuration. diff --git a/frigate/config/config.py b/frigate/config/config.py index 7aa6dac59d..2bc990c21a 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -326,6 +326,47 @@ def verify_required_zones_exist(camera_config: CameraConfig) -> None: ) +def verify_profile_overrides_match_base(camera_config: CameraConfig) -> None: + """Verify that profile zone and mask IDs reference entries defined on the base camera.""" + for profile_name, profile in camera_config.profiles.items(): + if profile.zones: + for zone_name in profile.zones: + if zone_name not in camera_config.zones: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"zone '{zone_name}' that does not exist on the base config" + ) + + if profile.motion and profile.motion.mask: + for mask_name in profile.motion.mask: + if mask_name not in camera_config.motion.mask: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"motion mask '{mask_name}' that does not exist on the base config" + ) + + if profile.objects: + for mask_name in profile.objects.mask or {}: + if mask_name not in (camera_config.objects.mask or {}): + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"object mask '{mask_name}' that does not exist on the base config" + ) + for label, filter_config in (profile.objects.filters or {}).items(): + base_filter = (camera_config.objects.filters or {}).get(label) + profile_filter_masks = ( + filter_config.mask if filter_config else None + ) or {} + base_filter_masks = (base_filter.mask if base_filter else None) or {} + for mask_name in profile_filter_masks: + if mask_name not in base_filter_masks: + raise ValueError( + f"Camera '{camera_config.name}' profile '{profile_name}' defines " + f"object mask '{mask_name}' for '{label}' that does not exist " + f"on the base config" + ) + + def verify_autotrack_zones(camera_config: CameraConfig) -> ValueError | None: """Verify that required_zones are specified when autotracking is enabled.""" if ( @@ -952,6 +993,7 @@ class FrigateConfig(FrigateBaseModel): verify_recording_segments_setup_with_reasonable_time(camera_config) verify_zone_objects_are_tracked(camera_config) verify_required_zones_exist(camera_config) + verify_profile_overrides_match_base(camera_config) verify_autotrack_zones(camera_config) verify_motion_and_detect(camera_config) verify_objects_track(camera_config, labelmap_objects) diff --git a/frigate/test/test_profiles.py b/frigate/test/test_profiles.py index b73fa74a08..51c3d78292 100644 --- a/frigate/test/test_profiles.py +++ b/frigate/test/test_profiles.py @@ -178,6 +178,141 @@ class TestCameraProfileConfig(unittest.TestCase): with self.assertRaises(ValidationError): FrigateConfig(**config_data) + def test_profile_zone_without_base_rejected(self): + """Profile defining a zone not present on the base camera is rejected.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "zones": { + "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"}, + }, + "profiles": { + "armed": { + "zones": { + "phantom": { + "coordinates": "0,0,50,0,50,50,0,50", + }, + }, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError) as ctx: + FrigateConfig(**config_data) + self.assertIn("phantom", str(ctx.exception)) + + def test_profile_motion_mask_without_base_rejected(self): + """Profile defining a motion mask not present on the base camera is rejected.""" + from pydantic import ValidationError + + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "motion": { + "mask": { + "base_mask": { + "coordinates": "0,0,100,0,100,100,0,100", + }, + }, + }, + "profiles": { + "armed": { + "motion": { + "mask": { + "phantom_mask": { + "coordinates": "0,0,50,0,50,50,0,50", + }, + }, + }, + }, + }, + }, + }, + } + with self.assertRaises(ValidationError) as ctx: + FrigateConfig(**config_data) + self.assertIn("phantom_mask", str(ctx.exception)) + + def test_profile_overrides_matching_base_accepted(self): + """Profile overrides that reference existing base zones/masks parse cleanly.""" + config_data = { + "mqtt": {"host": "mqtt"}, + "profiles": { + "armed": {"friendly_name": "Armed"}, + }, + "cameras": { + "front": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://10.0.0.1:554/video", + "roles": ["detect"], + } + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + "zones": { + "front_yard": {"coordinates": "0,0,100,0,100,100,0,100"}, + }, + "motion": { + "mask": { + "tree": { + "coordinates": "0,0,100,0,100,100,0,100", + }, + }, + }, + "profiles": { + "armed": { + "zones": { + "front_yard": { + "coordinates": "0,0,50,0,50,50,0,50", + "inertia": 5, + }, + }, + "motion": { + "mask": { + "tree": { + "coordinates": "0,0,75,0,75,75,0,75", + }, + }, + }, + }, + }, + }, + }, + } + config = FrigateConfig(**config_data) + assert "armed" in config.cameras["front"].profiles + class TestProfileInConfig(unittest.TestCase): """Test that profiles parse correctly in FrigateConfig.""" diff --git a/web/src/components/input/NameAndIdFields.tsx b/web/src/components/input/NameAndIdFields.tsx index c78a2917b1..a3bbf56fed 100644 --- a/web/src/components/input/NameAndIdFields.tsx +++ b/web/src/components/input/NameAndIdFields.tsx @@ -26,6 +26,7 @@ type NameAndIdFieldsProps = { placeholderName?: string; placeholderId?: string; idVisible?: boolean; + idDisabled?: boolean; }; export default function NameAndIdFields({ @@ -41,6 +42,7 @@ export default function NameAndIdFields({ placeholderName, placeholderId, idVisible, + idDisabled, }: NameAndIdFieldsProps) { const { t } = useTranslation(["common"]); const { watch, setValue, trigger, formState } = useFormContext(); @@ -59,6 +61,9 @@ export default function NameAndIdFields({ const effectiveProcessId = processId || defaultProcessId; useEffect(() => { + if (idDisabled) { + return; + } const subscription = watch((value, { name }) => { if (name === nameField) { hasUserTypedRef.current = true; @@ -68,7 +73,15 @@ export default function NameAndIdFields({ } }); return () => subscription.unsubscribe(); - }, [watch, setValue, trigger, nameField, idField, effectiveProcessId]); + }, [ + watch, + setValue, + trigger, + nameField, + idField, + effectiveProcessId, + idDisabled, + ]); // Auto-expand if there's an error on the ID field after user has typed useEffect(() => { @@ -123,6 +136,7 @@ export default function NameAndIdFields({ diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index 4fa09837ca..961b0f9277 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -258,8 +258,9 @@ export default function MotionMaskEditPane({ }, ); updateConfig(); - // Only publish WS state for base config when mask has a name - if (!editingProfile && maskName) { + // Only publish WS state for base config when mask has a name and + // wasn't renamed (the hook is bound to the old name). + if (!editingProfile && maskName && !renamingMask) { sendMotionMaskState(enabled ? "ON" : "OFF"); } } else { @@ -414,6 +415,7 @@ export default function MotionMaskEditPane({ nameLabel={t("masksAndZones.motionMasks.name.title")} nameDescription={t("masksAndZones.motionMasks.name.description")} placeholderName={t("masksAndZones.motionMasks.name.placeholder")} + idDisabled={!!editingProfile && polygon.name.length > 0} /> 0} /> 0; + + const idDisabled = useMemo(() => { + if (!isExistingZone || !polygon) { + return false; + } + if (editingProfile) { + return true; + } + const cam = config?.cameras[polygon.camera]; + if (!cam) { + return false; + } + const inRequiredZones = + cam.review.alerts.required_zones.includes(polygon.name) || + cam.review.detections.required_zones.includes(polygon.name); + const hasProfileOverride = Object.values(cam.profiles ?? {}).some( + (profile) => profile?.zones && polygon.name in profile.zones, + ); + return inRequiredZones || hasProfileOverride; + }, [config, polygon, editingProfile, isExistingZone]); + const cameraConfig = useMemo(() => { if (polygon?.camera && config) { return config.cameras[polygon.camera]; @@ -419,6 +441,7 @@ export default function ZoneEditPane({ toast.error(t("toast.save.error.noMessage", { ns: "common" }), { position: "top-center", }); + setIsLoading(false); return; } @@ -444,6 +467,7 @@ export default function ZoneEditPane({ toast.error(t("toast.save.error.noMessage", { ns: "common" }), { position: "top-center", }); + setIsLoading(false); return; } } @@ -527,8 +551,9 @@ export default function ZoneEditPane({ }, ); updateConfig(); - // Only publish WS state for base config when zone has a name - if (!editingProfile && polygon?.name) { + // Only publish WS state for base config when zone has a name and + // wasn't renamed (the hook is bound to the old name). + if (!editingProfile && polygon?.name && !renamingZone) { sendZoneState(enabled ? "ON" : "OFF"); } } else { @@ -650,6 +675,7 @@ export default function ZoneEditPane({ nameLabel={t("masksAndZones.zones.name.title")} nameDescription={t("masksAndZones.zones.name.tips")} placeholderName={t("masksAndZones.zones.name.inputPlaceHolder")} + idDisabled={idDisabled} /> Date: Mon, 25 May 2026 08:04:45 -0500 Subject: [PATCH 43/94] add motion review docs (#23307) --- docs/docs/configuration/motion_detection.md | 4 +++ docs/docs/configuration/review.md | 40 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md index 3f31d27dba..1695a03e9e 100644 --- a/docs/docs/configuration/motion_detection.md +++ b/docs/docs/configuration/motion_detection.md @@ -197,3 +197,7 @@ This option is handy when you want to prevent large transient changes from trigg When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise — they typically only take up a few megabytes and are quick to scan in the timeline UI. ::: + +## Reviewing Detected Motion + +To review what the detector picked up — or to search past recordings for motion in a specific region — see [Reviewing Motion](review.md#reviewing-motion) on the Review page. diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md index be02bdd8e9..fecb0b0f44 100644 --- a/docs/docs/configuration/review.md +++ b/docs/docs/configuration/review.md @@ -130,3 +130,43 @@ By default a review item will be created if any `review -> alerts -> labels` and Because zones don't apply to audio, audio labels will always be marked as a detection by default. ::: + +## Reviewing Motion + +The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above — see [Tuning Motion Detection](motion_detection.md) for how the underlying motion detector is configured. + +### Motion Previews + +The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects. + +On the page, click the 3-dots menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out. + +The pane provides a few controls: + +- **Speed** — speeds up or slows down all of the preview clips at once. +- **Dim** — controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility. +- **Filter** — opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway. + +Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage. + +### Motion Search + +Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked). + +To start a search, click the 3-dots menu on a camera in the page and choose **Motion Search**. In the dialog: + +1. Pick the camera and time range to scan. +2. Draw a polygon on the camera frame to define the region of interest. +3. Adjust the search parameters if needed: + +| Field | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Sensitivity Threshold** | Per-pixel luminance change required to count as motion inside the ROI. Behaves like Frigate's motion detection `threshold` setting. | +| **Minimum Change Area** | Minimum percentage of the region of interest that must change for a frame to be considered significant. Raise it to ignore small movements (leaves, distant motion); lower it when the object you care about only covers a small slice of the ROI. | +| **Frame Skip** | Number of frames to skip between samples — at a camera recording 20 fps, a skip value of 20 takes motion samples roughly once per second. Higher values scan much faster and are usually the right choice; lower it only when you need to catch the exact appearance or disappearance of a fast-moving object. | +| **Maximum Results** | Maximum number of matching timestamps to return. | +| **Parallel mode** | Process multiple recording segments in parallel. Speeds up large time ranges at the cost of higher CPU usage. | + +Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened. + +The status panel shows live progress and metrics such as how many segments were scanned, how many were skipped because no motion was recorded for that segment (using the stored motion heatmap), how many frames were decoded, and the total wall-clock time. Segments with no recorded motion in the selected ROI are skipped automatically, which is what makes searching long time ranges practical. From 88f944fe81c2ff382e7bc25ab1b099ce4812359c Mon Sep 17 00:00:00 2001 From: Ban <3637117+Ban921@users.noreply.github.com> Date: Wed, 27 May 2026 21:04:41 +0800 Subject: [PATCH 44/94] feat: add Traditional Chinese (zh-Hant) language option (#23322) The zh-Hant translations are synced from Weblate (98% complete) but the locale was never registered in the language selector, so users could not select it. Register zh-Hant in supportedLanguageKeys, add its display label, and map it to the zh-TW date-fns locale. --- web/public/locales/en/common.json | 1 + web/src/components/menu/GeneralSettings.tsx | 1 + web/src/hooks/use-date-locale.ts | 2 ++ web/src/lib/const.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/web/public/locales/en/common.json b/web/public/locales/en/common.json index 4436808d08..272e0f2795 100644 --- a/web/public/locales/en/common.json +++ b/web/public/locales/en/common.json @@ -177,6 +177,7 @@ "en": "English (English)", "es": "Español (Spanish)", "zhCN": "简体中文 (Simplified Chinese)", + "zhHant": "繁體中文 (Traditional Chinese)", "hi": "हिन्दी (Hindi)", "fr": "Français (French)", "ar": "العربية (Arabic)", diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index b3ab45f987..88d834d4dc 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -109,6 +109,7 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { "nb-NO": "nb", "yue-Hant": "yue", "zh-CN": "zhCN", + "zh-Hant": "zhHant", "pt-BR": "ptBR", }; diff --git a/web/src/hooks/use-date-locale.ts b/web/src/hooks/use-date-locale.ts index 4feef686a4..9cd018c423 100644 --- a/web/src/hooks/use-date-locale.ts +++ b/web/src/hooks/use-date-locale.ts @@ -34,6 +34,8 @@ const localeMap: Record Promise> = { sk: () => import("date-fns/locale/sk").then((module) => module.sk), "yue-Hant": () => import("date-fns/locale/zh-HK").then((module) => module.zhHK), + "zh-Hant": () => + import("date-fns/locale/zh-TW").then((module) => module.zhTW), lt: () => import("date-fns/locale/lt").then((module) => module.lt), th: () => import("date-fns/locale/th").then((module) => module.th), ca: () => import("date-fns/locale/ca").then((module) => module.ca), diff --git a/web/src/lib/const.ts b/web/src/lib/const.ts index 5db13e375d..2802870201 100644 --- a/web/src/lib/const.ts +++ b/web/src/lib/const.ts @@ -29,6 +29,7 @@ export const supportedLanguageKeys = [ "nb-NO", "sv", "zh-CN", + "zh-Hant", "yue-Hant", "ja", "vi", From 2858662be915c96ddf1940d95fda8199f0e38a83 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 27 May 2026 10:19:11 -0500 Subject: [PATCH 45/94] Miscellaneous fixes (#23317) * resolve global record.export.hwaccel_args to fix phantom camera override * auto-stop debug replay sessions after 12 hours * docs tweaks * add more tips to object classification docs * tweak language * Store hwaccel errors with timeout so it can retry * Add error logs for Intel GPU stats * add area --------- Co-authored-by: Nicolas Mowen --- .../object_classification.md | 9 +++- docs/docs/configuration/review.md | 4 +- docs/docs/troubleshooting/dummy-camera.md | 15 +++++++ frigate/api/fastapi_app.py | 8 +++- frigate/config/config.py | 7 +++ frigate/debug_replay.py | 45 +++++++++++++++++++ frigate/stats/emitter.py | 2 +- frigate/stats/util.py | 37 ++++++++++----- frigate/util/services.py | 12 +++++ 9 files changed, 123 insertions(+), 16 deletions(-) diff --git a/docs/docs/configuration/custom_classification/object_classification.md b/docs/docs/configuration/custom_classification/object_classification.md index 6368190705..05b110bdaf 100644 --- a/docs/docs/configuration/custom_classification/object_classification.md +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -149,9 +149,16 @@ For more detail, see [Frigate Tip: Best Practices for Training Face and Custom C - **The wizard is just the starting point**: You don't need to find and label every class upfront. Missing classes will naturally appear in Recent Classifications, and those images tend to be more valuable because they represent new conditions and edge cases. - **Problem framing**: Keep classes visually distinct and relevant to the chosen object types. - **Preprocessing**: Ensure examples reflect object crops similar to Frigate's boxes; keep the subject centered. -- **Labels**: Keep label names short and consistent; include a `none` class if you plan to ignore uncertain predictions for sub labels. +- **Crop size**: Aim for crops of at least 100×100 pixels (a 10,000 pixel area). Crops smaller than ~80×80 get stretched 3-7× by the model's 224×224 input resize and tend to collapse into a generic "blob" region of feature space where identity becomes unreliable. If most of your detections are small because the camera is far from the subject, consider repositioning the camera for closer crops. +- **Class balance**: Aim to keep your largest class within ~3× the count of your smallest. Beyond that, the model becomes biased toward the dominant class and tends to default borderline predictions to it (the "everything looks like Buddy" failure mode). - **Threshold**: Tune `threshold` per model to reduce false assignments. Start at `0.8` and adjust based on validation. +:::tip `none` works differently from named classes + +Named classes work best with visually uniform examples — every Buddy photo should look like Buddy. The `none` class needs the opposite: visual diversity across sizes, framings, and qualities, because at inference it has to absorb everything that isn't one of your named classes. Don't apply the same "only keep large, well-framed images" rule to `none` that you would to a named class. Mix in small crops, partial views, and false positives deliberately - otherwise the model has no signal for "small/ambiguous thing = not one of my known classes" and will force those crops into a named class by default. + +::: + ## Debugging Classification Models To troubleshoot issues with object classification models, enable debug logging to see detailed information about classification attempts, scores, and consensus calculations. diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md index fecb0b0f44..9f160c9487 100644 --- a/docs/docs/configuration/review.md +++ b/docs/docs/configuration/review.md @@ -139,7 +139,7 @@ The Review page also can show periods of motion that didn't produce a tracked ob The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects. -On the page, click the 3-dots menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out. +On the page, click the kebab menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out. The pane provides a few controls: @@ -153,7 +153,7 @@ Clicking a preview clip seeks the recording player to that timestamp so you can Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked). -To start a search, click the 3-dots menu on a camera in the page and choose **Motion Search**. In the dialog: +To start a search, click the kebab menu on a camera in the page and choose **Motion Search**. In the dialog: 1. Pick the camera and time range to scan. 2. Draw a polygon on the camera frame to define the region of interest. diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md index 7e9831e4be..4443d11e81 100644 --- a/docs/docs/troubleshooting/dummy-camera.md +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -3,6 +3,8 @@ id: dummy-camera title: Analyzing Object Detection --- +import NavPath from "@site/src/components/NavPath"; + Frigate provides several tools for investigating object detection and tracking behavior: reviewing recorded detections through the UI, using the built-in Debug Replay feature, and manually setting up a dummy camera for advanced scenarios. ## Reviewing Detections in the UI @@ -51,12 +53,25 @@ Only one replay session can be active at a time. If a session is already running ::: +### Starting Debug Replay + +Debug Replay can be started from several places in the UI. The starting point determines the time range that gets replayed. + +- **History — Actions menu.** Navigate to , open the **Actions** menu in the toolbar, and choose **Debug Replay**. From here you can pick a preset (**Last 1 Minute**, **Last 5 Minutes**), select a range directly on the timeline with **From Timeline**, or enter exact start and end times with **Custom**. This is the most flexible option and the best choice when you want to add padding around a detection. On mobile, the same options appear in the Actions drawer. +- **History — Detail Stream event menu.** While viewing a review item in the Detail Stream, open the menu on a tracked object's event card and choose **Debug Replay**. The replay range is set automatically to that object's start and end times. +- **Explore — search result menu.** From an Explore card, open the kebab menu and choose **Debug Replay**. The range is taken from the tracked object's lifecycle. +- **Explore — Tracking Details Actions menu.** Open a tracked object's **Tracking Details** dialog, then choose **Debug Replay** from the Actions menu. Same automatic range as the search result menu. +- **Exports — export card menu.** From , open the menu on an export and choose **Debug Replay** to loop the exported clip through the detection pipeline for the camera it was exported from. + +The Detail Stream, Explore, and Exports entry points use the underlying recording or export's bounds with a small amount of padding. This can be convenient for quick checks, but if a detection is short or you want extra "settle" time for motion and the detector, start the replay from the History Actions menu instead and widen the range manually. + ### Variables to consider - The replay will not always produce identical results to the original run. Different frames may be selected on replay, which can change detections and tracking. - Motion detection depends on the exact frames used; small frame shifts can change motion regions and therefore what gets passed to the detector. - Object detection is not fully deterministic: models and post-processing can yield slightly different results across runs. - In cases where a detection is short and a replay may only be a small number of frames, it is recommended to manually add some padding before and after the detection so that the motion and object detectors have time to settle into the scene. Rather than starting Debug Replay from Explore, navigate to History for your camera, choose Debug Replay from the Actions menu, and click the "From Timeline" or "Custom" option. +- The replay camera inherits the source camera's zones. Any automations that trigger on those zone names will fire for the replay camera as well. This can be helpful when debugging zone behavior, but may be unexpected. You can add a condition on the source camera's name in your automation if you want to exclude replay triggers. Treat the replay as a close approximation rather than an exact reproduction. Run multiple loops and examine the debug overlays and logs to understand the behavior. diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index f201ab7135..3f8d8a7a5f 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,3 +1,4 @@ +import asyncio import logging import re from typing import Optional @@ -36,7 +37,7 @@ from frigate.comms.event_metadata_updater import ( from frigate.config import FrigateConfig from frigate.config.camera.updater import CameraConfigUpdatePublisher from frigate.config.profile_manager import ProfileManager -from frigate.debug_replay import DebugReplayManager +from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog from frigate.embeddings import EmbeddingsContext from frigate.genai import GenAIClientManager from frigate.ptz.onvif import OnvifController @@ -116,6 +117,11 @@ def create_fastapi_app( @app.on_event("startup") async def startup(): logger.info("FastAPI started") + asyncio.create_task( + debug_replay_auto_stop_watchdog( + replay_manager, frigate_config, config_publisher + ) + ) # Rate limiter (used for login endpoint) if frigate_config.auth.failed_login_rate_limit is None: diff --git a/frigate/config/config.py b/frigate/config/config.py index 2bc990c21a..1a12c20e51 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -680,6 +680,13 @@ class FrigateConfig(FrigateBaseModel): if self.ffmpeg.hwaccel_args == "auto": self.ffmpeg.hwaccel_args = auto_detect_hwaccel() + # Resolve global export hwaccel_args so it matches the per-camera + # resolution below. Without this, every camera reads as overriding + # record.export.hwaccel_args because the global stays "auto" while + # the camera value gets resolved to the actual args list. + if self.record.export.hwaccel_args == "auto": + self.record.export.hwaccel_args = self.ffmpeg.hwaccel_args + # Populate global audio filters from listen. Existing user-defined # entries for labels not in listen are preserved but unused at runtime. if self.audio.filters is None: diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index ea95e153c1..956bc20012 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -5,6 +5,7 @@ frigate.jobs.debug_replay. This module owns only session presence (active), session metadata, and post-session cleanup. """ +import asyncio import logging import os import shutil @@ -40,6 +41,9 @@ from frigate.util.config import find_config_file logger = logging.getLogger(__name__) +MAX_SESSION_DURATION_SECONDS = 12 * 60 * 60 +AUTO_STOP_CHECK_INTERVAL_SECONDS = 60 + class DebugReplayManager: """Owns the lifecycle pointers for a single debug replay session. @@ -58,6 +62,7 @@ class DebugReplayManager: self.clip_path: str | None = None self.start_ts: float | None = None self.end_ts: float | None = None + self.session_started_at: float | None = None self._job_state_publisher = JobStatePublisher() @property @@ -83,6 +88,7 @@ class DebugReplayManager: self.start_ts = start_ts self.end_ts = end_ts self.clip_path = None + self.session_started_at = time.time() def mark_session_ready(self, clip_path: str) -> None: """Record the on-disk clip path after the camera has been published.""" @@ -104,6 +110,7 @@ class DebugReplayManager: self.clip_path = None self.start_ts = None self.end_ts = None + self.session_started_at = None def publish_camera( self, @@ -351,3 +358,41 @@ def cleanup_replay_cameras() -> None: shutil.rmtree(REPLAY_DIR) except Exception as e: logger.error("Failed to remove replay cache directory: %s", e) + + +async def debug_replay_auto_stop_watchdog( + manager: DebugReplayManager, + frigate_config: FrigateConfig, + config_publisher: CameraConfigUpdatePublisher, +) -> None: + """Auto-stop debug replay sessions that exceed MAX_SESSION_DURATION_SECONDS. + + Backstop against a session left running for days. The cap is intentionally + generous so realistic tuning and overnight soak workflows aren't disrupted. + """ + while True: + try: + await asyncio.sleep(AUTO_STOP_CHECK_INTERVAL_SECONDS) + + started_at = manager.session_started_at + if not manager.active or started_at is None: + continue + + if time.time() - started_at < MAX_SESSION_DURATION_SECONDS: + continue + + replay_name = manager.replay_camera_name + await asyncio.to_thread( + manager.stop, + frigate_config=frigate_config, + config_publisher=config_publisher, + ) + logger.info( + "Debug replay auto-stopped after exceeding max session duration of %d hours: %s", + MAX_SESSION_DURATION_SECONDS // 3600, + replay_name, + ) + except asyncio.CancelledError: + raise + except Exception: + logger.exception("Error in debug replay auto-stop watchdog") diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 2b34c7c4ed..13f50c5868 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -32,7 +32,7 @@ class StatsEmitter(threading.Thread): self.config = config self.stats_tracking = stats_tracking self.stop_event = stop_event - self.hwaccel_errors: list[str] = [] + self.hwaccel_errors: dict[str, float] = {} self.stats_history: list[dict[str, Any]] = [] # create communication for stats diff --git a/frigate/stats/util.py b/frigate/stats/util.py index a0141d1305..56efce5d01 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -1,6 +1,7 @@ """Utilities for stats.""" import asyncio +import logging import os import shutil import time @@ -34,6 +35,10 @@ from frigate.util.services import ( ) from frigate.version import VERSION +logger = logging.getLogger(__name__) + +HWACCEL_ERROR_COOLDOWN_SECONDS = 3600 + def get_latest_version(config: FrigateConfig) -> str: if not config.telemetry.version_check: @@ -167,7 +172,9 @@ def get_detector_stats( def get_processing_stats( - config: FrigateConfig, stats: dict[str, str], hwaccel_errors: list[str] + config: FrigateConfig, + stats: dict[str, str], + hwaccel_errors: dict[str, float], ) -> None: """Get stats for cpu / gpu.""" @@ -206,7 +213,9 @@ async def set_bandwidth_stats(config: FrigateConfig, all_stats: dict[str, Any]) async def set_gpu_stats( - config: FrigateConfig, all_stats: dict[str, Any], hwaccel_errors: list[str] + config: FrigateConfig, + all_stats: dict[str, Any], + hwaccel_errors: dict[str, float], ) -> None: """Parse GPUs from hwaccel args and use for stats.""" hwaccel_args = [] @@ -231,12 +240,16 @@ async def set_gpu_stats( stats: dict[str, dict] = {} intel_gpu_collected = False + now = time.monotonic() for args in hwaccel_args: - if args in hwaccel_errors: - # known erroring args should automatically return as error - stats["error-gpu"] = {"gpu": "", "mem": ""} - elif "cuvid" in args or "nvidia" in args: + last_error = hwaccel_errors.get(args) + if last_error is not None: + if now - last_error < HWACCEL_ERROR_COOLDOWN_SECONDS: + continue + hwaccel_errors.pop(args, None) + + if "cuvid" in args or "nvidia" in args: # nvidia GPU nvidia_usage = get_nvidia_gpu_stats() @@ -253,7 +266,7 @@ async def set_gpu_stats( else: stats["nvidia-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "nvmpi" in args or "jetson" in args: # nvidia Jetson jetson_usage = get_jetson_stats() @@ -262,7 +275,7 @@ async def set_gpu_stats( stats["jetson-gpu"] = {"vendor": "nvidia", **jetson_usage} else: stats["jetson-gpu"] = {"vendor": "nvidia", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "qsv" in args or ("vaapi" in args and not is_vaapi_amd_driver()): if not config.telemetry.stats.intel_gpu_stats: continue @@ -280,7 +293,7 @@ async def set_gpu_stats( stats[name] = entry else: stats["intel-gpu"] = {"vendor": "intel", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "vaapi" in args: if not config.telemetry.stats.amd_gpu_stats: continue @@ -292,7 +305,7 @@ async def set_gpu_stats( stats["amd-vaapi"] = {"vendor": "amd", **amd_usage} else: stats["amd-vaapi"] = {"vendor": "amd", "gpu": "", "mem": ""} - hwaccel_errors.append(args) + hwaccel_errors[args] = time.monotonic() elif "preset-rk" in args: rga_usage = get_rockchip_gpu_stats() @@ -328,7 +341,9 @@ async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> No def stats_snapshot( - config: FrigateConfig, stats_tracking: StatsTrackingTypes, hwaccel_errors: list[str] + config: FrigateConfig, + stats_tracking: StatsTrackingTypes, + hwaccel_errors: dict[str, float], ) -> dict[str, Any]: """Get a snapshot of the current stats that are being tracked.""" camera_metrics = stats_tracking["camera_metrics"] diff --git a/frigate/util/services.py b/frigate/util/services.py index a5b1af8249..0445053b8a 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -416,6 +416,11 @@ def get_intel_gpu_stats( snapshot_a = _read_intel_drm_fdinfo(target_pdev) if not snapshot_a: + logger.warning( + "Unable to collect Intel GPU stats: no DRM fdinfo entries found" + "%s. Check that /proc is readable and the i915/xe driver is loaded", + f" for pdev {target_pdev}" if target_pdev else "", + ) return None start = time.monotonic() @@ -424,6 +429,9 @@ def get_intel_gpu_stats( snapshot_b = _read_intel_drm_fdinfo(target_pdev) if not snapshot_b or elapsed_ns <= 0: + logger.warning( + "Unable to collect Intel GPU stats: second DRM fdinfo sample was empty" + ) return None def _new_engine_pct() -> dict[str, float]: @@ -464,6 +472,10 @@ def get_intel_gpu_stats( pid_pct[data_b["pid"]] = pid_pct.get(data_b["pid"], 0.0) + client_total if not per_pdev_engine_pct: + logger.warning( + "Unable to collect Intel GPU stats: no per-engine counters available " + "(i915 requires kernel >= 5.19)" + ) return None names = intel_gpu_name_resolver.get_names() From e9ef4f978ad8ba7f546cb2a473639ad5bbae3b31 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 27 May 2026 13:03:09 -0500 Subject: [PATCH 46/94] Restore runtime state on startup (#23326) * add class * restore runtime state in dispatcher * restore on startup with special case for profile * add tests * update docs * mypy --- docs/docs/configuration/live.md | 11 +- docs/docs/configuration/profiles.md | 2 + docs/docs/integrations/mqtt.md | 10 +- frigate/api/app.py | 5 + frigate/app.py | 9 +- frigate/comms/dispatcher.py | 62 +++++ frigate/comms/runtime_state.py | 163 +++++++++++++ frigate/config/profile_manager.py | 20 +- frigate/test/test_dispatcher_runtime_state.py | 217 ++++++++++++++++++ frigate/test/test_profiles.py | 49 ++++ frigate/test/test_runtime_state.py | 136 +++++++++++ 11 files changed, 676 insertions(+), 8 deletions(-) create mode 100644 frigate/comms/runtime_state.py create mode 100644 frigate/test/test_dispatcher_runtime_state.py create mode 100644 frigate/test/test_runtime_state.py diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index bc58d50bbc..b6c0e288f1 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -262,7 +262,7 @@ cameras: Each camera has three possible states, surfaced as a status selector in **Settings → Global configuration → Camera management**: - **On** — streams are processed normally. Object detection, recording, and Live view are active. -- **Off** — Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. This state does **not** persist across Frigate restarts; the camera returns to On after a restart. +- **Off** — Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. The Off state persists across Frigate restarts via a `.runtime_state.json` file alongside `config.yml` (see [Runtime toggle persistence](#runtime-toggle-persistence)). - **Disabled** — the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.** #### Turning a camera on or off @@ -290,6 +290,15 @@ For both Off and Disabled cameras, go2rtc remains active but does not use system If you want a camera's historical data (review items, tracked objects, footage) to stay accessible in the UI while you stop processing, set the camera to **Off**. If you want the camera fully removed from the Live dashboard, review filters, and other UI surfaces, set it to **Disabled**. The Disabled state still keeps the camera in Camera management so it can be re-enabled later; if you want to remove all traces of a camera including its configuration, delete it via Camera management instead. +#### Runtime toggle persistence + +The Live view toggles for **camera on/off**, **detect**, **recordings**, **snapshots**, and **audio detection** — along with the equivalent MQTT `/set` topics — write the new state to `.runtime_state.json` next to your `config.yml`. The file is replayed on Frigate startup so your last-known toggle states survive a restart. Two interactions worth knowing: + +- **Settings UI saves win.** When you save a field through **Settings → Global configuration**, the matching entry is cleared from `.runtime_state.json` so the new value in your config file is the durable source. +- **Switching profiles clears all runtime overrides.** Activating or deactivating a [profile](/configuration/profiles) is treated as a deliberate state change, so the file is wiped to avoid stale overrides replaying on top of the new profile. + +If you hand-edit `config.yml` while runtime overrides exist, the overrides will still replay on restart. Delete `.runtime_state.json` to reset to the YAML-defined defaults. + ### Live player error messages When your browser runs into problems playing back your camera streams, it will log short error messages to the browser console. They indicate playback, codec, or network issues on the client/browser side, not something server side with Frigate itself. Below are the common messages you may see and simple actions you can take to try to resolve them. diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md index a7b3b2f50d..4d93168f81 100644 --- a/docs/docs/configuration/profiles.md +++ b/docs/docs/configuration/profiles.md @@ -130,6 +130,8 @@ Profiles can be activated and deactivated via the Frigate UI, [MQTT](/integratio In the Frigate UI, open the Settings cog and select **Profiles** from the submenu to see all defined profiles. From there you can activate any profile or deactivate the current one. The active profile is indicated in the UI so you always know which profile is in effect. +Activating or deactivating a profile clears any [runtime toggle overrides](/configuration/live#runtime-toggle-persistence) so the profile's settings aren't silently undone by a stale toggle from before the switch. + ## Example: Home / Away Setup A common use case is having different detection and notification settings based on whether you are home or away. This example below is for a system with two cameras, `front_door` and `indoor_cam`. diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 03f5135f4b..44d6ecee0b 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -368,7 +368,7 @@ The published value is the detected state class name (e.g., `open`, `closed`, `o ### `frigate//enabled/set` -Topic to turn Frigate's processing of a camera on or off at runtime. Expected values are `ON` and `OFF`. The change is **not** persisted across Frigate restarts — the camera returns to the configured state on restart. To permanently disable a camera, use **Settings → Global configuration → Camera management** in the Frigate UI. See [Camera state](/configuration/live#camera-state) for the difference between turning a camera off and disabling it. +Topic to turn Frigate's processing of a camera on or off at runtime. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). To permanently change the configured value, use **Settings → Global configuration → Camera management** in the Frigate UI. See [Camera state](/configuration/live#camera-state) for the difference between turning a camera off and disabling it. ### `frigate//enabled/state` @@ -376,7 +376,7 @@ Topic with current runtime state of processing for a camera. Published values ar ### `frigate//detect/set` -Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn object detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). ### `frigate//detect/state` @@ -384,7 +384,7 @@ Topic with current state of object detection for a camera. Published values are ### `frigate//audio/set` -Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn audio detection for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). ### `frigate//audio/state` @@ -392,7 +392,7 @@ Topic with current state of audio detection for a camera. Published values are ` ### `frigate//recordings/set` -Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). ### `frigate//recordings/state` @@ -400,7 +400,7 @@ Topic with current state of recordings for a camera. Published values are `ON` a ### `frigate//snapshots/set` -Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`. +Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)). ### `frigate//snapshots/state` diff --git a/frigate/api/app.py b/frigate/api/app.py index 35eed2b9ce..8ed365fce8 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -908,6 +908,11 @@ def config_set(request: Request, body: AppConfigSetBody): status_code=500, ) + # drop runtime overrides for any fields the user just rewrote in + # yaml so a stale override doesn't silently win after restart + if request.app.dispatcher is not None: + request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys()) + if body.requires_restart == 0 or body.update_topic: old_config: FrigateConfig = request.app.frigate_config request.app.frigate_config = config diff --git a/frigate/app.py b/frigate/app.py index 8b5766148b..b2402d2326 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -348,7 +348,11 @@ class FrigateApp: persisted in cam.profiles for cam in self.config.cameras.values() ): logger.info("Restoring persisted profile '%s'", persisted) - self.profile_manager.activate_profile(persisted) + # don't clear runtime overrides here, restore_runtime_state() later + # in startup replays it on top of the activated profile + self.profile_manager.activate_profile( + persisted, clear_runtime_overrides=False + ) def start_detectors(self) -> None: for name in self.config.cameras.keys(): @@ -612,6 +616,9 @@ class FrigateApp: self.start_record_cleanup() self.start_watchdog() + # restore persisted runtime overrides on top of config + self.dispatcher.restore_runtime_state() + self.init_auth() try: diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 27d5ef1255..a85e644940 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -3,11 +3,13 @@ import datetime import json import logging +from collections.abc import Iterable from typing import Any, Callable, Optional, cast from frigate.camera import PTZMetrics from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager from frigate.comms.base_communicator import Communicator +from frigate.comms.runtime_state import RuntimeStatePersistence from frigate.comms.webpush import WebPushClient from frigate.config import BirdseyeModeEnum, FrigateConfig from frigate.config.camera.updater import ( @@ -67,6 +69,7 @@ class Dispatcher: self.embeddings_reindex: dict[str, Any] = {} self.birdseye_layout: dict[str, Any] = {} self.audio_transcription_state: str = "idle" + self._runtime_state = RuntimeStatePersistence() self._camera_settings_handlers: dict[str, Callable] = { "audio": self._on_audio_command, "audio_transcription": self._on_audio_transcription_command, @@ -397,6 +400,60 @@ class Dispatcher: for comm in self.comms: comm.stop() + def restore_runtime_state(self) -> None: + """Replay persisted runtime overrides through the camera settings handlers. + + Called once after Frigate startup completes so processing threads can + receive the resulting ``config_updater`` broadcasts. Unknown cameras + and topics are skipped; handler exceptions are logged and replay + continues for remaining entries. + """ + state = self._runtime_state.load() + for camera_name, features in state.items(): + if camera_name not in self.config.cameras: + continue + for topic, value in features.items(): + handler = self._camera_settings_handlers.get(topic) + if handler is None: + continue + payload = "ON" if value else "OFF" + try: + handler(camera_name, payload) + except Exception: + logger.exception( + "Failed to restore runtime state %s.%s=%s", + camera_name, + topic, + payload, + ) + continue + logger.info( + "Restored runtime state: %s.%s=%s", + camera_name, + topic, + payload, + ) + + def clear_runtime_state_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None: + """Clear stored runtime overrides for YAML keys that were just rewritten. + + Called by ``/api/config/set`` after a successful YAML save so an + explicit settings-UI save isn't silently overridden by an older + runtime toggle on the next restart. + """ + self._runtime_state.clear_for_yaml_keys(dotted_keys) + + def clear_runtime_state(self) -> None: + """Wipe every stored runtime override. + + Called when a profile is activated or deactivated. A profile switch + changes the layer below the runtime overrides, so the stored + "steady state" is no longer valid and must be reset; otherwise a + subsequent restart would replay stale overrides on top of the new + profile-derived in-memory state. + """ + self._runtime_state.clear_all() + def _on_detect_command(self, camera_name: str, payload: str) -> None: """Callback for detect topic.""" detect_settings = self.config.cameras[camera_name].detect @@ -428,6 +485,7 @@ class Dispatcher: CameraConfigUpdateTopic(CameraConfigUpdateEnum.detect, camera_name), detect_settings, ) + self._runtime_state.set(camera_name, "detect", detect_settings.enabled) self.publish(f"{camera_name}/detect/state", payload, retain=True) def _on_enabled_command(self, camera_name: str, payload: str) -> None: @@ -452,6 +510,7 @@ class Dispatcher: CameraConfigUpdateTopic(CameraConfigUpdateEnum.enabled, camera_name), camera_settings.enabled, ) + self._runtime_state.set(camera_name, "enabled", camera_settings.enabled) self.publish(f"{camera_name}/enabled/state", payload, retain=True) def _on_motion_command(self, camera_name: str, payload: str) -> None: @@ -614,6 +673,7 @@ class Dispatcher: CameraConfigUpdateTopic(CameraConfigUpdateEnum.audio, camera_name), audio_settings, ) + self._runtime_state.set(camera_name, "audio", audio_settings.enabled) self.publish(f"{camera_name}/audio/state", payload, retain=True) def _on_audio_transcription_command(self, camera_name: str, payload: str) -> None: @@ -670,6 +730,7 @@ class Dispatcher: CameraConfigUpdateTopic(CameraConfigUpdateEnum.record, camera_name), record_settings, ) + self._runtime_state.set(camera_name, "recordings", record_settings.enabled) self.publish(f"{camera_name}/recordings/state", payload, retain=True) def _on_snapshots_command(self, camera_name: str, payload: str) -> None: @@ -689,6 +750,7 @@ class Dispatcher: CameraConfigUpdateTopic(CameraConfigUpdateEnum.snapshots, camera_name), snapshots_settings, ) + self._runtime_state.set(camera_name, "snapshots", snapshots_settings.enabled) self.publish(f"{camera_name}/snapshots/state", payload, retain=True) def _on_ptz_command(self, camera_name: str, payload: str | bytes) -> None: diff --git a/frigate/comms/runtime_state.py b/frigate/comms/runtime_state.py new file mode 100644 index 0000000000..5066ed3993 --- /dev/null +++ b/frigate/comms/runtime_state.py @@ -0,0 +1,163 @@ +"""Persistence layer for dispatcher runtime state overrides.""" + +import json +import logging +import os +from collections.abc import Iterable +from typing import Any + +from filelock import FileLock, Timeout + +from frigate.util.config import find_config_file + +logger = logging.getLogger(__name__) + + +class RuntimeStatePersistence: + """Persist last-known runtime states for dispatcher toggles. + + Stores boolean overrides applied to camera-level toggles by the dispatcher. + Overrides are replayed at startup on top of the YAML-derived in-memory + config, so changes made via MQTT or the live-view UI survive a restart. + """ + + # Maps dispatcher topic name -> YAML key suffix under cameras. + TRACKED_TOPICS: dict[str, str] = { + "enabled": "enabled", + "detect": "detect.enabled", + "snapshots": "snapshots.enabled", + "recordings": "record.enabled", + "audio": "audio.enabled", + } + + _SUFFIX_TO_TOPIC: dict[str, str] = {v: k for k, v in TRACKED_TOPICS.items()} + + def __init__(self) -> None: + self._path = os.path.join( + os.path.dirname(find_config_file()), ".runtime_state.json" + ) + self._lock_path = f"{self._path}.lock" + self._lock_timeout = 5 + + def load(self) -> dict[str, dict[str, bool]]: + """Return {camera: {topic: bool}} or {} if missing/corrupt.""" + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + except Timeout: + logger.error("Timed out acquiring runtime state lock for load") + return {} + cameras = data.get("cameras", {}) + if not isinstance(cameras, dict): + return {} + # Filter out malformed camera entries so callers can trust the shape. + return { + name: features + for name, features in cameras.items() + if isinstance(features, dict) + } + + def set(self, camera: str, topic: str, value: bool) -> None: + """Persist a single (camera, topic, value). No-op if topic untracked.""" + if topic not in self.TRACKED_TOPICS: + return + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + cameras = data.setdefault("cameras", {}) + if not isinstance(cameras, dict): + cameras = {} + data["cameras"] = cameras + cam = cameras.setdefault(camera, {}) + if not isinstance(cam, dict): + cam = {} + cameras[camera] = cam + cam[topic] = bool(value) + self._write_locked(data) + except Timeout: + logger.error("Timed out persisting runtime state for %s/%s", camera, topic) + except OSError: + logger.exception("Failed to persist runtime state for %s/%s", camera, topic) + + def clear_all(self) -> None: + """Wipe every stored runtime override. + + Called when the "layer below" changes in a way that invalidates all + runtime overrides for the current session (currently: profile + activation or deactivation). + """ + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + if not os.path.exists(self._path): + return + self._write_locked({"cameras": {}}) + except Timeout: + logger.error("Timed out clearing runtime state") + except OSError: + logger.exception("Failed to clear runtime state") + + def clear_for_yaml_keys(self, dotted_keys: Iterable[str]) -> None: + """Remove stored entries whose YAML key was just rewritten. + + Each dotted key must be of the form ``cameras..``. + Keys that don't match a tracked topic are ignored. + """ + to_remove: list[tuple[str, str]] = [] + for key in dotted_keys: + parts = key.split(".") + if len(parts) < 3 or parts[0] != "cameras": + continue + camera = parts[1] + suffix = ".".join(parts[2:]) + topic = self._SUFFIX_TO_TOPIC.get(suffix) + if topic is not None: + to_remove.append((camera, topic)) + + if not to_remove: + return + + try: + with FileLock(self._lock_path, timeout=self._lock_timeout): + data = self._read_locked() + cameras = data.get("cameras") + if not isinstance(cameras, dict): + return + changed = False + for camera, topic in to_remove: + cam = cameras.get(camera) + if isinstance(cam, dict) and topic in cam: + del cam[topic] + changed = True + if not cam: + del cameras[camera] + if changed: + self._write_locked(data) + except Timeout: + logger.error("Timed out clearing runtime state for YAML keys") + except OSError: + logger.exception("Failed to clear runtime state for YAML keys") + + def _read_locked(self) -> dict[str, Any]: + """Read the JSON file while the FileLock is held. + + Returns ``{}`` on a missing or corrupt file so the caller can write a + fresh structure on the next mutation. + """ + if not os.path.exists(self._path): + return {} + try: + with open(self._path, "r") as f: + data = json.load(f) + except (OSError, json.JSONDecodeError): + logger.exception( + "Failed to read runtime state file %s; starting fresh", self._path + ) + return {} + return data if isinstance(data, dict) else {} + + def _write_locked(self, data: dict[str, Any]) -> None: + """Atomically write the JSON file while the FileLock is held.""" + tmp_path = f"{self._path}.tmp" + with open(tmp_path, "w") as f: + json.dump(data, f, indent=2, sort_keys=True) + os.replace(tmp_path, self._path) diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py index d109bdecbc..6aba8f1942 100644 --- a/frigate/config/profile_manager.py +++ b/frigate/config/profile_manager.py @@ -124,11 +124,24 @@ class ProfileManager: self.config.active_profile = None self._persist_active_profile(None) - def activate_profile(self, profile_name: Optional[str]) -> Optional[str]: + # drop all runtime overrides so they don't replay stale values on restart + if self.dispatcher is not None: + self.dispatcher.clear_runtime_state() + + def activate_profile( + self, + profile_name: Optional[str], + clear_runtime_overrides: bool = True, + ) -> Optional[str]: """Activate a profile by name, or deactivate if None. Args: profile_name: Profile name to activate, or None to deactivate. + clear_runtime_overrides: When True (the default, for user-initiated + activations) drop the dispatcher's runtime override file because + the layer below changed. Startup callers that are replaying a + persisted profile pass False so the runtime state stays + available for the subsequent replay step. Returns: None on success, or an error message string on failure. @@ -156,6 +169,11 @@ class ProfileManager: self.config.active_profile = profile_name self._persist_active_profile(profile_name) + + # a profile switch invalidates the steady-state runtime overrides + if clear_runtime_overrides and self.dispatcher is not None: + self.dispatcher.clear_runtime_state() + logger.info( "Profile %s", f"'{profile_name}' activated" if profile_name else "deactivated", diff --git a/frigate/test/test_dispatcher_runtime_state.py b/frigate/test/test_dispatcher_runtime_state.py new file mode 100644 index 0000000000..dae0518d80 --- /dev/null +++ b/frigate/test/test_dispatcher_runtime_state.py @@ -0,0 +1,217 @@ +"""Tests for Dispatcher runtime state persistence wiring.""" + +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from frigate.comms.dispatcher import Dispatcher +from frigate.comms.runtime_state import RuntimeStatePersistence + + +def _make_camera_mock( + *, + enabled: bool = True, + enabled_in_config: bool = True, + detect_enabled: bool = True, + record_enabled: bool = True, + record_enabled_in_config: bool = True, + snapshots_enabled: bool = True, + audio_enabled: bool = True, + audio_enabled_in_config: bool = True, +) -> MagicMock: + """Build a camera config mock with the fields the in-scope handlers read.""" + camera = MagicMock() + camera.enabled = enabled + camera.enabled_in_config = enabled_in_config + camera.detect.enabled = detect_enabled + camera.motion.enabled = True # avoid the detect→motion side-effect path + camera.record.enabled = record_enabled + camera.record.enabled_in_config = record_enabled_in_config + camera.snapshots.enabled = snapshots_enabled + camera.audio.enabled = audio_enabled + camera.audio.enabled_in_config = audio_enabled_in_config + return camera + + +def _build_dispatcher(cameras: dict[str, MagicMock]) -> Dispatcher: + """Construct a Dispatcher with the bare-minimum mocks the tests need.""" + config = MagicMock() + config.cameras = cameras + config_updater = MagicMock() + onvif = MagicMock() + ptz_metrics: dict = {} + communicators: list = [] + + with ( + patch("frigate.comms.dispatcher.CameraActivityManager"), + patch("frigate.comms.dispatcher.AudioActivityManager"), + ): + return Dispatcher(config, config_updater, onvif, ptz_metrics, communicators) + + +class TestRestoreRuntimeState(unittest.TestCase): + """Verify replay routes through handlers and tolerates missing entries.""" + + def setUp(self) -> None: + self.dispatcher = _build_dispatcher( + { + "front_door": _make_camera_mock(), + "back_yard": _make_camera_mock(), + } + ) + # Swap each in-scope handler for a MagicMock so we can assert calls + # without exercising the handler's own logic. + self.handler_mocks: dict[str, MagicMock] = {} + for topic in ("enabled", "detect", "snapshots", "recordings", "audio"): + mock = MagicMock() + self.dispatcher._camera_settings_handlers[topic] = mock + self.handler_mocks[topic] = mock + + def test_replays_each_stored_entry_through_its_handler(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock( + return_value={ + "front_door": {"detect": False, "recordings": False}, + "back_yard": {"audio": False}, + } + ), + ) + self.dispatcher.restore_runtime_state() + + self.handler_mocks["detect"].assert_called_once_with("front_door", "OFF") + self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF") + self.handler_mocks["audio"].assert_called_once_with("back_yard", "OFF") + self.handler_mocks["enabled"].assert_not_called() + self.handler_mocks["snapshots"].assert_not_called() + + def test_skips_unknown_cameras(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"removed_cam": {"detect": False}}), + ) + self.dispatcher.restore_runtime_state() + for mock in self.handler_mocks.values(): + mock.assert_not_called() + + def test_skips_unknown_topics(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"front_door": {"some_old_topic": True}}), + ) + self.dispatcher.restore_runtime_state() + for mock in self.handler_mocks.values(): + mock.assert_not_called() + + def test_continues_after_handler_exception(self) -> None: + self.handler_mocks["detect"].side_effect = RuntimeError("boom") + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock( + return_value={ + "front_door": {"detect": False, "recordings": False}, + } + ), + ) + # Must not raise; the recordings handler must still run. + self.dispatcher.restore_runtime_state() + self.handler_mocks["recordings"].assert_called_once_with("front_door", "OFF") + + def test_true_value_routes_as_on_payload(self) -> None: + self.dispatcher._runtime_state = MagicMock( + spec=RuntimeStatePersistence, + load=MagicMock(return_value={"front_door": {"detect": True}}), + ) + self.dispatcher.restore_runtime_state() + self.handler_mocks["detect"].assert_called_once_with("front_door", "ON") + + +class TestHandlersPersistViaSet(unittest.TestCase): + """Verify each in-scope handler writes to the runtime state on success.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.mkdtemp() + self.config_path = os.path.join(self.tmp_dir, "config.yml") + with open(self.config_path, "w") as f: + f.write("") + self._patcher = patch( + "frigate.comms.runtime_state.find_config_file", + return_value=self.config_path, + ) + self._patcher.start() + + # Start with everything OFF so each ON payload triggers a real change + self.cameras = { + "front_door": _make_camera_mock( + enabled=False, + detect_enabled=False, + record_enabled=False, + snapshots_enabled=False, + audio_enabled=False, + ) + } + self.dispatcher = _build_dispatcher(self.cameras) + + def tearDown(self) -> None: + self._patcher.stop() + for name in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, name)) + os.rmdir(self.tmp_dir) + + def _stored_state(self) -> dict: + return RuntimeStatePersistence().load() + + def test_enabled_handler_persists(self) -> None: + self.dispatcher._on_enabled_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"enabled": True}}) + + def test_detect_handler_persists(self) -> None: + self.dispatcher._on_detect_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"detect": True}}) + + def test_recordings_handler_persists(self) -> None: + self.dispatcher._on_recordings_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"recordings": True}}) + + def test_snapshots_handler_persists(self) -> None: + self.dispatcher._on_snapshots_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"snapshots": True}}) + + def test_audio_handler_persists(self) -> None: + self.dispatcher._on_audio_command("front_door", "ON") + self.assertEqual(self._stored_state(), {"front_door": {"audio": True}}) + + def test_enabled_in_config_gate_blocks_persistence(self) -> None: + """An ON payload rejected by the gate must not be persisted.""" + cam = self.cameras["front_door"] + cam.enabled_in_config = False + cam.record.enabled_in_config = False + cam.audio.enabled_in_config = False + + self.dispatcher._on_enabled_command("front_door", "ON") + self.dispatcher._on_recordings_command("front_door", "ON") + self.dispatcher._on_audio_command("front_door", "ON") + + self.assertEqual(self._stored_state(), {}) + + +class TestClearPassthrough(unittest.TestCase): + """The dispatcher's public clear methods delegate to the store.""" + + def test_clear_runtime_state_for_yaml_keys_passthrough(self) -> None: + dispatcher = _build_dispatcher({}) + dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence) + keys = ["cameras.front_door.detect.enabled"] + dispatcher.clear_runtime_state_for_yaml_keys(keys) + dispatcher._runtime_state.clear_for_yaml_keys.assert_called_once_with(keys) + + def test_clear_runtime_state_passthrough(self) -> None: + dispatcher = _build_dispatcher({}) + dispatcher._runtime_state = MagicMock(spec=RuntimeStatePersistence) + dispatcher.clear_runtime_state() + dispatcher._runtime_state.clear_all.assert_called_once_with() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_profiles.py b/frigate/test/test_profiles.py index 51c3d78292..6766b39163 100644 --- a/frigate/test/test_profiles.py +++ b/frigate/test/test_profiles.py @@ -727,6 +727,55 @@ class TestProfileManager(unittest.TestCase): # Should not raise json.dumps(api_base) + @patch.object(ProfileManager, "_persist_active_profile") + def test_activate_profile_clears_dispatcher_runtime_state(self, mock_persist): + """User-initiated activation drops runtime overrides (steady-state rule).""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_deactivate_profile_clears_dispatcher_runtime_state(self, mock_persist): + """Deactivating a profile also drops runtime overrides.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.reset_mock() + + manager.activate_profile(None) + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_startup_replay_does_not_clear_runtime_state(self, mock_persist): + """Startup callers pass clear_runtime_overrides=False to preserve state.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed", clear_runtime_overrides=False) + dispatcher.clear_runtime_state.assert_not_called() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_update_config_clears_when_active_profile_reapplies(self, mock_persist): + """After /api/config/set, an active-profile re-application drops state.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + manager.activate_profile("armed") + dispatcher.clear_runtime_state.reset_mock() + + new_config = FrigateConfig(**self.config_data) + manager.update_config(new_config) + dispatcher.clear_runtime_state.assert_called_once_with() + + @patch.object(ProfileManager, "_persist_active_profile") + def test_update_config_does_not_clear_when_no_active_profile(self, mock_persist): + """Plain /api/config/set without a profile doesn't trigger the broad clear.""" + dispatcher = MagicMock() + manager = ProfileManager(self.config, self.mock_updater, dispatcher) + # No activate_profile call — config.active_profile is None + new_config = FrigateConfig(**self.config_data) + manager.update_config(new_config) + dispatcher.clear_runtime_state.assert_not_called() + class TestProfilePersistence(unittest.TestCase): """Test profile persistence to disk.""" diff --git a/frigate/test/test_runtime_state.py b/frigate/test/test_runtime_state.py new file mode 100644 index 0000000000..6143184030 --- /dev/null +++ b/frigate/test/test_runtime_state.py @@ -0,0 +1,136 @@ +"""Tests for RuntimeStatePersistence.""" + +import json +import os +import tempfile +import unittest +from unittest.mock import patch + +from frigate.comms.runtime_state import RuntimeStatePersistence + + +class TestRuntimeStatePersistence(unittest.TestCase): + """Unit tests for the JSON-backed runtime state store.""" + + def setUp(self) -> None: + self.tmp_dir = tempfile.mkdtemp() + self.config_path = os.path.join(self.tmp_dir, "config.yml") + # Touch a placeholder config.yml so find_config_file returns a real path + with open(self.config_path, "w") as f: + f.write("") + self._patcher = patch( + "frigate.comms.runtime_state.find_config_file", + return_value=self.config_path, + ) + self._patcher.start() + self.store = RuntimeStatePersistence() + + def tearDown(self) -> None: + self._patcher.stop() + for name in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, name)) + os.rmdir(self.tmp_dir) + + def test_load_returns_empty_when_file_missing(self) -> None: + self.assertEqual(self.store.load(), {}) + + def test_set_then_load_round_trip(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", True) + self.store.set("back_yard", "audio", False) + + result = self.store.load() + self.assertEqual( + result, + { + "front_door": {"detect": False, "recordings": True}, + "back_yard": {"audio": False}, + }, + ) + + def test_set_with_untracked_topic_is_noop(self) -> None: + self.store.set("front_door", "ptz_autotracker", True) + self.assertEqual(self.store.load(), {}) + # File should not even be created if no tracked entries were written + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + self.assertFalse(os.path.exists(runtime_path)) + + def test_set_overwrites_previous_value(self) -> None: + self.store.set("front_door", "detect", True) + self.store.set("front_door", "detect", False) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_load_returns_empty_when_file_corrupt(self) -> None: + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + with open(runtime_path, "w") as f: + f.write("{not valid json") + self.assertEqual(self.store.load(), {}) + + def test_load_handles_unexpected_top_level_shape(self) -> None: + runtime_path = os.path.join(self.tmp_dir, ".runtime_state.json") + with open(runtime_path, "w") as f: + json.dump(["unexpected", "list"], f) + self.assertEqual(self.store.load(), {}) + + def test_clear_for_yaml_keys_removes_matching_entries(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", False) + self.store.set("back_yard", "audio", False) + + self.store.clear_for_yaml_keys( + [ + "cameras.front_door.detect.enabled", + "cameras.back_yard.audio.enabled", + ] + ) + + self.assertEqual( + self.store.load(), + {"front_door": {"recordings": False}}, + ) + + def test_clear_for_yaml_keys_collapses_empty_camera_dict(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys(["cameras.front_door.detect.enabled"]) + self.assertEqual(self.store.load(), {}) + + def test_clear_for_yaml_keys_ignores_unrelated_keys(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys( + [ + "ui.theme", + "go2rtc.streams.x", + "cameras.front_door.ffmpeg.inputs", + "not_cameras.front_door.detect.enabled", + ] + ) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_clear_for_yaml_keys_handles_empty_iterable(self) -> None: + self.store.set("front_door", "detect", False) + self.store.clear_for_yaml_keys([]) + self.assertEqual(self.store.load(), {"front_door": {"detect": False}}) + + def test_camera_level_enabled_uses_top_level_yaml_key(self) -> None: + """`enabled` topic maps to the camera-level `cameras..enabled` key.""" + self.store.set("front_door", "enabled", False) + self.store.clear_for_yaml_keys(["cameras.front_door.enabled"]) + self.assertEqual(self.store.load(), {}) + + def test_clear_all_wipes_every_entry(self) -> None: + self.store.set("front_door", "detect", False) + self.store.set("front_door", "recordings", True) + self.store.set("back_yard", "audio", False) + + self.store.clear_all() + + self.assertEqual(self.store.load(), {}) + + def test_clear_all_is_safe_when_file_missing(self) -> None: + # No prior set() calls — file does not exist + self.store.clear_all() + self.assertEqual(self.store.load(), {}) + + +if __name__ == "__main__": + unittest.main() From 50f17e68522419794086ead3752519f9bf966264 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 27 May 2026 14:35:07 -0500 Subject: [PATCH 47/94] Add live streams widget (#23330) * add live streams widget * i18n * docs --- docs/docs/configuration/live.md | 14 +- web/public/locales/en/views/settings.json | 11 + .../config-form/section-configs/live.ts | 13 +- .../theme/fields/LiveStreamsField.tsx | 346 ++++++++++++++++++ .../config-form/theme/fields/index.ts | 1 + .../config-form/theme/frigateTheme.ts | 2 + 6 files changed, 383 insertions(+), 4 deletions(-) create mode 100644 web/src/components/config-form/theme/fields/LiveStreamsField.tsx diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index b6c0e288f1..d27447f1bc 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -88,8 +88,18 @@ Configure a "friendly name" for your stream followed by the go2rtc stream name. -1. Navigate to , then select your camera. - - Under **Live stream names**, add entries mapping a friendly name to each go2rtc stream name (e.g., `Main Stream` mapped to `test_cam`, `Sub Stream` mapped to `test_cam_sub`). +1. Navigate to and select your camera. +2. Under **Live stream names**, click **Add stream** to add a new entry. +3. In the **Stream name** field, enter a friendly name that will appear in the Live UI's stream dropdown (e.g., `Main Stream`). +4. In the **go2rtc stream** field, open the dropdown and select the go2rtc stream this name should map to (e.g., `test_cam`). The dropdown lists every stream configured under `go2rtc.streams`. If the go2rtc stream hasn't been created yet, you can type the name and choose **Use "..."** to save a custom value. +5. Repeat for each additional stream you want to expose (e.g., `Sub Stream` → `test_cam_sub`). +6. Use the trash icon on a row to remove a stream, then **Save** the section. + +:::tip + +Configure your go2rtc streams first under so the dropdown is populated with valid options. + +::: diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 9731eb22dc..966f83d725 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1405,6 +1405,17 @@ "namePlaceholder": "e.g., Wife's Car", "platePlaceholder": "Plate number or regex" }, + "liveStreams": { + "streamNameLabel": "Stream name", + "streamNamePlaceholder": "e.g., Main HD Stream", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Select a go2rtc stream", + "go2rtcStreamSearch": "Search or enter a stream name…", + "noGo2rtcStreams": "No go2rtc streams configured", + "availableStreams": "Available streams", + "useCustom": "Use \"{{value}}\"", + "addStream": "Add stream" + }, "timezone": { "defaultOption": "Use browser timezone" }, diff --git a/web/src/components/config-form/section-configs/live.ts b/web/src/components/config-form/section-configs/live.ts index c0d80627c2..c0026ec8da 100644 --- a/web/src/components/config-form/section-configs/live.ts +++ b/web/src/components/config-form/section-configs/live.ts @@ -4,17 +4,26 @@ const live: SectionConfigOverrides = { base: { sectionDocs: "/configuration/live", restartRequired: [], - fieldOrder: ["stream_name", "height", "quality"], + fieldOrder: ["streams", "height", "quality"], fieldGroups: {}, hiddenFields: ["enabled_in_config"], advancedFields: ["height", "quality"], }, global: { - restartRequired: ["stream_name", "height", "quality"], + restartRequired: ["streams", "height", "quality"], hiddenFields: ["streams"], }, camera: { restartRequired: ["height", "quality"], + uiSchema: { + streams: { + "ui:field": "LiveStreamsField", + "ui:options": { + label: false, + suppressDescription: true, + }, + }, + }, }, }; diff --git a/web/src/components/config-form/theme/fields/LiveStreamsField.tsx b/web/src/components/config-form/theme/fields/LiveStreamsField.tsx new file mode 100644 index 0000000000..b74547ba1b --- /dev/null +++ b/web/src/components/config-form/theme/fields/LiveStreamsField.tsx @@ -0,0 +1,346 @@ +import type { FieldPathList, FieldProps, RJSFSchema } from "@rjsf/utils"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown, Plus } from "lucide-react"; +import { LuPlus, LuTrash2 } from "react-icons/lu"; +import type { ConfigFormContext } from "@/types/configForm"; +import get from "lodash/get"; +import { isSubtreeModified } from "../utils"; + +type LiveStreamsData = Record; + +type StreamValueComboboxProps = { + id: string; + value: string; + options: string[]; + disabled?: boolean; + readonly?: boolean; + onChange: (next: string) => void; +}; + +function StreamValueCombobox({ + id, + value, + options, + disabled, + readonly, + onChange, +}: StreamValueComboboxProps) { + const { t } = useTranslation(["views/settings", "common"]); + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + const trimmedSearch = searchValue.trim(); + const matchesOption = useMemo( + () => options.some((o) => o.toLowerCase() === trimmedSearch.toLowerCase()), + [options, trimmedSearch], + ); + const showCustomOption = trimmedSearch.length > 0 && !matchesOption; + + const commit = (next: string) => { + onChange(next); + setSearchValue(""); + setOpen(false); + }; + + const placeholder = t("configForm.liveStreams.go2rtcStreamPlaceholder", { + ns: "views/settings", + }); + const searchPlaceholder = t("configForm.liveStreams.go2rtcStreamSearch", { + ns: "views/settings", + }); + const noStreams = t("configForm.liveStreams.noGo2rtcStreams", { + ns: "views/settings", + }); + const availableHeading = t("configForm.liveStreams.availableStreams", { + ns: "views/settings", + }); + + return ( + { + setOpen(next); + if (!next) setSearchValue(""); + }} + > + + + + + + { + if (e.key === "Enter" && showCustomOption) { + e.preventDefault(); + commit(trimmedSearch); + } + }} + /> + + {showCustomOption && ( + + commit(trimmedSearch)} + > + + {t("configForm.liveStreams.useCustom", { + ns: "views/settings", + value: trimmedSearch, + })} + + + )} + {options.length > 0 ? ( + + {options.map((option) => ( + commit(option)} + > + + {option} + + ))} + + ) : !showCustomOption ? ( +
    + {noStreams} +
    + ) : null} +
    +
    +
    +
    + ); +} + +export function LiveStreamsField(props: FieldProps) { + const { schema, formData, onChange, idSchema, disabled, readonly } = props; + const formContext = props.registry?.formContext as + | ConfigFormContext + | undefined; + + const configNamespace = + formContext?.i18nNamespace ?? + (formContext?.level === "camera" ? "config/cameras" : "config/global"); + const { t: fallbackT } = useTranslation(["common", configNamespace]); + const t = formContext?.t ?? fallbackT; + + const data: LiveStreamsData = useMemo(() => { + if (!formData || typeof formData !== "object" || Array.isArray(formData)) { + return {}; + } + return formData as LiveStreamsData; + }, [formData]); + + const entries = useMemo(() => Object.entries(data), [data]); + + const id = idSchema?.$id ?? props.name; + const sectionPrefix = formContext?.sectionI18nPrefix; + + const title = + t(`${sectionPrefix}.${id}.label`) ?? (schema as RJSFSchema).title; + const description = + t(`${sectionPrefix}.${id}.description`) ?? + (schema as RJSFSchema).description; + + const go2rtcStreamNames = useMemo(() => { + const streams = formContext?.fullConfig?.go2rtc?.streams; + if (!streams || typeof streams !== "object") return []; + return Object.keys(streams).sort(); + }, [formContext?.fullConfig?.go2rtc?.streams]); + + const emptyPath = useMemo(() => [] as FieldPathList, []); + const fieldPath = + (props as { fieldPathId?: { path?: FieldPathList } }).fieldPathId?.path ?? + emptyPath; + + const isModified = useMemo(() => { + const baselineRoot = formContext?.baselineFormData; + const baselineValue = baselineRoot + ? get(baselineRoot, fieldPath) + : undefined; + return isSubtreeModified( + data, + baselineValue, + formContext?.overrides, + fieldPath, + formContext?.formData, + ); + }, [fieldPath, formContext, data]); + + const handleAddEntry = useCallback(() => { + const next = { ...data, "": "" }; + onChange(next, fieldPath); + }, [data, fieldPath, onChange]); + + const handleRemoveEntry = useCallback( + (key: string) => { + const next = { ...data }; + delete next[key]; + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const handleRenameKey = useCallback( + (oldKey: string, newKey: string) => { + if (oldKey === newKey) return; + const next: LiveStreamsData = {}; + for (const [k, v] of Object.entries(data)) { + if (k === oldKey) { + next[newKey] = v; + } else { + next[k] = v; + } + } + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const handleUpdateValue = useCallback( + (key: string, value: string) => { + const next = { ...data, [key]: value }; + onChange(next, fieldPath); + }, + [data, fieldPath, onChange], + ); + + const baseId = idSchema?.$id || "live_streams"; + const deleteLabel = t("button.delete", { + ns: "common", + defaultValue: "Delete", + }); + const streamNameLabel = t("configForm.liveStreams.streamNameLabel", { + ns: "views/settings", + }); + const streamNamePlaceholder = t( + "configForm.liveStreams.streamNamePlaceholder", + { ns: "views/settings" }, + ); + const go2rtcStreamLabel = t("configForm.liveStreams.go2rtcStreamLabel", { + ns: "views/settings", + }); + const addStreamLabel = t("configForm.liveStreams.addStream", { + ns: "views/settings", + }); + + return ( + + + + {title} + + {description && ( +

    {description}

    + )} +
    + + {entries.map(([key, value], entryIndex) => { + const entryId = `${baseId}-${entryIndex}`; + return ( +
    +
    + + handleRenameKey(key, e.target.value)} + /> +
    +
    + + handleUpdateValue(key, next)} + /> +
    +
    + +
    +
    + ); + })} + +
    + +
    +
    +
    + ); +} + +export default LiveStreamsField; diff --git a/web/src/components/config-form/theme/fields/index.ts b/web/src/components/config-form/theme/fields/index.ts index b6b7078661..4bb91ed352 100644 --- a/web/src/components/config-form/theme/fields/index.ts +++ b/web/src/components/config-form/theme/fields/index.ts @@ -2,3 +2,4 @@ export { LayoutGridField } from "./LayoutGridField"; export { DetectorHardwareField } from "./DetectorHardwareField"; export { ReplaceRulesField } from "./ReplaceRulesField"; +export { LiveStreamsField } from "./LiveStreamsField"; diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index ebc6b19b35..40f3f76c93 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -51,6 +51,7 @@ import { ReplaceRulesField } from "./fields/ReplaceRulesField"; import { CameraInputsField } from "./fields/CameraInputsField"; import { DictAsYamlField } from "./fields/DictAsYamlField"; import { KnownPlatesField } from "./fields/KnownPlatesField"; +import { LiveStreamsField } from "./fields/LiveStreamsField"; export interface FrigateTheme { widgets: RegistryWidgetsType; @@ -109,5 +110,6 @@ export const frigateTheme: FrigateTheme = { CameraInputsField: CameraInputsField, DictAsYamlField: DictAsYamlField, KnownPlatesField: KnownPlatesField, + LiveStreamsField: LiveStreamsField, }, }; From bc65713ae4df0ba1ebe438cc5db863eab1c69f97 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 28 May 2026 18:44:06 -0500 Subject: [PATCH 48/94] Clone camera settings (#23339) * add clone dialog * i18n * tweaks * add to camera management pane * add e2e test * optional disable portal prop * radio and checkbox tweaks * tweak i18n * add select all/select none * fixes * reset form only on open transition * unselect all targets for existing camera * fix test * reorder sections for save and collapse to single put for new camera * change source and allow cloning to multiple cameras * tweak language * fix overflowing text in save all popover * tweaks * fix per label object masks * use grid for source and target * language tweak --- web/e2e/specs/clone-camera.spec.ts | 181 +++ web/public/locales/en/views/settings.json | 86 ++ .../overlay/detail/SaveAllPreviewPopover.tsx | 13 +- .../components/settings/CloneCameraDialog.tsx | 1046 +++++++++++++++++ web/src/utils/cameraClone.ts | 856 ++++++++++++++ web/src/utils/configUtil.ts | 1 + .../views/settings/CameraManagementView.tsx | 53 +- 7 files changed, 2223 insertions(+), 13 deletions(-) create mode 100644 web/e2e/specs/clone-camera.spec.ts create mode 100644 web/src/components/settings/CloneCameraDialog.tsx create mode 100644 web/src/utils/cameraClone.ts diff --git a/web/e2e/specs/clone-camera.spec.ts b/web/e2e/specs/clone-camera.spec.ts new file mode 100644 index 0000000000..1c75e71a3d --- /dev/null +++ b/web/e2e/specs/clone-camera.spec.ts @@ -0,0 +1,181 @@ +/** + * Camera clone dialog E2E tests. + * + * Covers the design invariants that don't depend on per-camera resolution + * differences in the mock fixture: + * 1. Dialog opens from the "Clone settings" button below Add/Delete. + * 2. A source camera must be chosen inside the dialog before cloning. + * 3. "Stream URLs and roles" is forced on and disabled for new-camera target. + * 4. Cloning to a new camera issues a single add PUT and shows a restart prompt. + * 5. The existing-camera target selects multiple destinations via a switch + * popover (with an "All cameras" toggle and source exclusion); the closed + * trigger summarizes the selection by name or as "All cameras". + * + * The spatial-mismatch warning path is exercised in unit-level review and via + * manual QA — the shared mock fixture ships every camera at 1280×720. The + * existing-camera PUT fan-out is likewise not asserted here: the mock cameras + * are identical apart from stream URLs (which existing-camera clones never + * copy) and the schema mock is empty, so a clone onto them produces no diff + * and no PUT. That path is covered by unit-level review and manual QA. + */ + +import { test, expect } from "../fixtures/frigate-test"; + +async function openCloneDialog(frigateApp: { + page: import("@playwright/test").Page; +}) { + await frigateApp.page + .getByRole("button", { name: /^Clone settings$/i }) + .click(); + await expect(frigateApp.page.getByRole("dialog")).toBeVisible(); +} + +async function selectSource( + frigateApp: { page: import("@playwright/test").Page }, + source: string, +) { + await frigateApp.page.getByRole("dialog").getByRole("combobox").click(); + await frigateApp.page + .getByRole("option", { name: source, exact: true }) + .click(); +} + +test.describe("Camera clone dialog @medium @mobile", () => { + test.beforeEach(async ({ frigateApp }) => { + await frigateApp.goto("/settings?page=cameraManagement"); + await expect( + frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }), + ).toBeVisible(); + }); + + test("opens the dialog from the Clone settings button", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + + await expect( + frigateApp.page.getByRole("dialog").getByText(/Clone camera settings/i), + ).toBeVisible(); + + // The Clone button is disabled until a source (and target) is chosen. + await expect( + frigateApp.page.getByRole("button", { name: /^Clone$/i }), + ).toBeDisabled(); + }); + + test("forces Stream URLs and roles on for new-camera target", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + // The "New camera" radio is selected by default; the Streams group renders + // the ffmpeg_live checkbox as forced-checked and disabled. + const streamsLabel = frigateApp.page + .locator("label") + .filter({ hasText: /Stream URLs and roles/i }); + await expect(streamsLabel).toBeVisible(); + + const streamsCheckbox = streamsLabel.getByRole("checkbox"); + await expect(streamsCheckbox).toBeChecked(); + await expect(streamsCheckbox).toBeDisabled(); + }); + + test("issues a single add PUT and shows restart toast for new-camera target", async ({ + frigateApp, + }) => { + const requests: { body: unknown }[] = []; + + await frigateApp.page.route("**/api/config/set", async (route) => { + const body = route.request().postDataJSON(); + requests.push({ body }); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true, require_restart: false }), + }); + }); + + await frigateApp.goto("/settings?page=cameraManagement"); + await expect( + frigateApp.page.getByRole("heading", { name: /Manage Cameras/i }), + ).toBeVisible(); + + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + const nameInput = frigateApp.page.getByPlaceholder( + /e\.g\., back_door or Back Door/i, + ); + await nameInput.fill("clone_target_one"); + + // With a source picked and a valid name, changeCount > 0 enables Clone. + await expect( + frigateApp.page.getByRole("button", { name: /^Clone$/i }), + ).toBeEnabled({ timeout: 5_000 }); + + await frigateApp.page.getByRole("button", { name: /^Clone$/i }).click(); + + // New-camera clones bundle into a single atomic add PUT (avoids + // per-section validation ordering issues). + await expect.poll(() => requests.length, { timeout: 10_000 }).toBe(1); + + const firstBody = requests[0].body as { + requires_restart?: number; + update_topic?: string; + }; + expect(firstBody.update_topic).toMatch( + /config\/cameras\/clone_target_one\/add/, + ); + expect(firstBody.requires_restart).toBe(1); + + // The toast offers a Restart action because new-camera always needs restart. + // .first() avoids strict-mode rejection when both the toast action and the + // RestartDialog trigger render concurrently. + await expect( + frigateApp.page.getByRole("button", { name: /Restart/i }).first(), + ).toBeVisible({ timeout: 8_000 }); + }); + + test("selects multiple existing destination cameras via a switch popover", async ({ + frigateApp, + }) => { + await openCloneDialog(frigateApp); + await selectSource(frigateApp, "Front Door"); + + await frigateApp.page + .getByRole("radio", { name: /Existing cameras/i }) + .click(); + + const dialog = frigateApp.page.getByRole("dialog"); + + // The destination trigger starts with the empty-selection placeholder. + await dialog + .getByRole("button", { name: /Select at least one camera/i }) + .click(); + + // The chosen source is excluded from the destination switch list. + await expect( + dialog.getByRole("switch", { name: /Backyard/i }), + ).toBeVisible(); + await expect(dialog.getByRole("switch", { name: /Garage/i })).toBeVisible(); + await expect( + dialog.getByRole("switch", { name: /^Front Door$/i }), + ).toHaveCount(0); + + // Selecting a single camera summarizes by name once the popover closes. + await dialog.getByRole("switch", { name: /Backyard/i }).click(); + await frigateApp.page.keyboard.press("Escape"); + await expect( + dialog.getByRole("button", { name: /^Backyard$/i }), + ).toBeVisible(); + + // Reopen and select everything; the trigger collapses to "All cameras". + await dialog.getByRole("button", { name: /^Backyard$/i }).click(); + await dialog.getByRole("switch", { name: /^All cameras$/i }).click(); + await frigateApp.page.keyboard.press("Escape"); + await expect( + dialog.getByRole("button", { name: /^All cameras$/i }), + ).toBeVisible(); + }); +}); diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 966f83d725..2374c506e7 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -544,6 +544,92 @@ "normal": "Normal", "dedicatedLpr": "Dedicated LPR", "saveSuccess": "Updated camera type for {{cameraName}}. Restart Frigate to apply the changes." + }, + "clone": { + "sectionTitle": "Clone settings", + "sectionDescription": "Copy configuration from one camera to another camera or a new one.", + "button": "Clone settings", + "title": "Clone camera settings", + "description": "Copy a camera's configuration to one or more other cameras or a new camera. Identity (name, friendly name, web UI URL, display order) is never copied.", + "source": { + "label": "Source camera", + "placeholder": "Select a source camera", + "required": "Select a source camera" + }, + "target": { + "legend": "Target", + "newRadio": "New camera", + "newNameLabel": "Camera name", + "newNamePlaceholder": "e.g., back_door or Back Door", + "newNameRequired": "Camera name is required", + "newNameInvalid": "Invalid camera name", + "newNameCollision": "A camera with this name already exists", + "newStreamsForced": "Streams are always copied for a new camera.", + "existingCamerasRadio": "Existing cameras", + "allCameras": "All cameras", + "existingPlaceholder": "Select at least one camera", + "existingDisabled": "No other cameras to copy to" + }, + "categories": { + "legend": "Settings to clone", + "description": "Choose which settings to copy from the source camera.", + "selectAll": "Select all", + "selectNone": "Select none", + "resetDefaults": "Reset to defaults", + "general": "General", + "spatial": "Spatial settings", + "streams": "Streams", + "spatialWarningTitle": "Resolution mismatch", + "spatialWarning": "Source camera {{srcCamera}} detect resolution ({{srcWidth}}×{{srcHeight}}) differs from: {{cameras}}. Polygons may not align on those cameras. These defaults are off; enable to copy as-is.", + "restartHint": "Restart required", + "items": { + "record": "Recording", + "snapshots": "Snapshots", + "review": "Review", + "motion": "Motion detection", + "objects": "Objects", + "audio": "Audio detection", + "audio_transcription": "Audio transcription", + "notifications": "Notifications", + "birdseye": "Birdseye", + "mqtt": "MQTT", + "timestamp_style": "Timestamp style", + "onvif": "ONVIF", + "lpr": "License plate recognition", + "face_recognition": "Face recognition", + "semantic_search": "Semantic search", + "genai": "Generative AI", + "type": "Camera type (normal / dedicated LPR)", + "profiles": "Profiles", + "detect": "Detect dimensions", + "zones": "Zones", + "motion_mask": "Motion masks", + "object_masks": "Object masks", + "ffmpeg_live": "Stream URLs and roles" + } + }, + "footer": { + "changeCount_zero": "No changes selected", + "changeCount_one": "{{count}} change will be applied", + "changeCount_other": "{{count}} changes will be applied", + "restartNeeded": "Restart will be required for some changes.", + "liveOnly": "All changes will apply live without a restart.", + "submit": "Clone", + "submitting": "Cloning…" + }, + "toast": { + "success": "Settings copied to {{cameraName}}", + "successWithRestart": "Settings copied to {{cameraName}}. Restart Frigate to apply all changes.", + "successMulti_one": "Settings copied to {{count}} camera", + "successMulti_other": "Settings copied to {{count}} cameras", + "successMultiWithRestart_one": "Settings copied to {{count}} camera. Restart Frigate to apply all changes.", + "successMultiWithRestart_other": "Settings copied to {{count}} cameras. Restart Frigate to apply all changes.", + "partialFailure": "{{successCount}} sections applied; '{{failedSection}}' failed: {{errorMessage}}", + "partialFailureMulti": "Copied to {{successCount}} camera(s); failed for {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Camera {{cameraName}} was created but some settings failed to copy: {{errorMessage}}", + "sourceMissing": "Source camera no longer exists", + "submitError": "Failed to clone camera: {{errorMessage}}" + } } }, "cameraReview": { diff --git a/web/src/components/overlay/detail/SaveAllPreviewPopover.tsx b/web/src/components/overlay/detail/SaveAllPreviewPopover.tsx index a775935315..e65b347de8 100644 --- a/web/src/components/overlay/detail/SaveAllPreviewPopover.tsx +++ b/web/src/components/overlay/detail/SaveAllPreviewPopover.tsx @@ -22,6 +22,7 @@ type SaveAllPreviewPopoverProps = { className?: string; align?: "start" | "center" | "end"; side?: "top" | "bottom" | "left" | "right"; + disablePortal?: boolean; }; export default function SaveAllPreviewPopover({ @@ -29,6 +30,7 @@ export default function SaveAllPreviewPopover({ className, align = "end", side = "bottom", + disablePortal = false, }: SaveAllPreviewPopoverProps) { const { t } = useTranslation(["views/settings", "common"]); const [open, setOpen] = useState(false); @@ -67,6 +69,7 @@ export default function SaveAllPreviewPopover({ event.preventDefault()} > @@ -108,13 +111,13 @@ export default function SaveAllPreviewPopover({ }`} className="rounded-md border border-secondary bg-background_alt p-2" > -
    +
    {t("saveAllPreview.scope.label", { ns: "views/settings", })} - {scopeLabel} + {scopeLabel} {item.profileName && ( <> @@ -122,7 +125,7 @@ export default function SaveAllPreviewPopover({ ns: "views/settings", })} - + {item.profileName} @@ -132,7 +135,7 @@ export default function SaveAllPreviewPopover({ ns: "views/settings", })} - + {item.fieldPath} @@ -140,7 +143,7 @@ export default function SaveAllPreviewPopover({ ns: "views/settings", })} - + {formatValue(item.value)}
    diff --git a/web/src/components/settings/CloneCameraDialog.tsx b/web/src/components/settings/CloneCameraDialog.tsx new file mode 100644 index 0000000000..61458211b8 --- /dev/null +++ b/web/src/components/settings/CloneCameraDialog.tsx @@ -0,0 +1,1046 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import useSWR, { mutate as swrMutate } from "swr"; +import axios from "axios"; +import { toast } from "sonner"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { isReplayCamera, processCameraName } from "@/utils/cameraUtil"; +import type { FrigateConfig } from "@/types/frigateConfig"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { LuArrowRight, LuChevronDown, LuTriangleAlert } from "react-icons/lu"; +import { + CLONE_CATEGORIES, + type CloneCategoryKey, + type CloneCategoryGroup, + type RawCameraPaths, + getCategoryDefaults, + resolutionsMatch, + buildClonedCameraPayloads, + buildClonePreviewItems, +} from "@/utils/cameraClone"; +import { buildConfigDataForPath } from "@/utils/configUtil"; +import { useConfigSchema } from "@/hooks/use-config-schema"; +import { useRestart } from "@/api/ws"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; +import RestartDialog from "@/components/overlay/dialog/RestartDialog"; +import ActivityIndicator from "@/components/indicators/activity-indicator"; +import SaveAllPreviewPopover from "@/components/overlay/detail/SaveAllPreviewPopover"; +import FilterSwitch from "@/components/filter/FilterSwitch"; + +type CloneCameraDialogProps = { + open: boolean; + onClose: () => void; +}; + +type CloneFormValues = { + sourceCamera: string; + targetMode: "new" | "existing"; + newName: string; + existingTargets: string[]; +}; + +export default function CloneCameraDialog({ + open, + onClose, +}: CloneCameraDialogProps) { + const { t } = useTranslation(["views/settings", "common"]); + const { data: config } = useSWR("config"); + const { data: rawPaths } = useSWR("config/raw_paths"); + const [isSubmitting, setIsSubmitting] = useState(false); + + const sourceCameras = useMemo(() => { + if (!config) return []; + return Object.keys(config.cameras) + .filter((c) => !isReplayCamera(c)) + .sort(); + }, [config]); + + const formSchema = useMemo(() => { + const reservedNames = new Set([ + ...(config ? Object.keys(config.cameras) : []), + ...(config?.go2rtc?.streams ? Object.keys(config.go2rtc.streams) : []), + ]); + return z + .object({ + sourceCamera: z.string(), + targetMode: z.enum(["new", "existing"]), + newName: z.string(), + existingTargets: z.array(z.string()), + }) + .superRefine((data, ctx) => { + if (!data.sourceCamera) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["sourceCamera"], + message: t("cameraManagement.clone.source.required"), + }); + } + if (data.targetMode === "new") { + const trimmed = data.newName.trim(); + if (!trimmed) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["newName"], + message: t("cameraManagement.clone.target.newNameRequired"), + }); + return; + } + const { finalCameraName } = processCameraName(trimmed); + if (!finalCameraName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["newName"], + message: t("cameraManagement.clone.target.newNameInvalid"), + }); + return; + } + if (reservedNames.has(finalCameraName)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["newName"], + message: t("cameraManagement.clone.target.newNameCollision"), + }); + } + } else if (data.existingTargets.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["existingTargets"], + message: t("cameraManagement.clone.target.existingPlaceholder"), + }); + } + }); + }, [config, t]); + + const form = useForm({ + resolver: zodResolver(formSchema), + mode: "onChange", + defaultValues: { + sourceCamera: "", + targetMode: "new", + newName: "", + existingTargets: [], + }, + }); + + const sourceCamera = form.watch("sourceCamera"); + const targetMode = form.watch("targetMode"); + const existingTargets = form.watch("existingTargets"); + + const targetIsNew = targetMode === "new"; + + const otherCameras = useMemo(() => { + if (!config) return []; + return Object.keys(config.cameras) + .filter((c) => c !== sourceCamera && !isReplayCamera(c)) + .sort(); + }, [config, sourceCamera]); + + const srcCfg = config?.cameras?.[sourceCamera]; + + // Existing targets whose detect resolution differs from the source. Spatial + // settings use detect-resolution coordinates, so cloning them to a camera + // with a different resolution is flagged (but still allowed). + const mismatchedTargets = useMemo(() => { + if (targetIsNew || !srcCfg?.detect) return []; + return existingTargets.filter((cam) => { + const dst = config?.cameras?.[cam]; + return dst?.detect && !resolutionsMatch(srcCfg.detect, dst.detect); + }); + }, [targetIsNew, srcCfg, existingTargets, config]); + + const allResMatch = mismatchedTargets.length === 0; + + const [selectedCategories, setSelectedCategories] = useState< + Set + >(() => getCategoryDefaults(true)); + + // Reset form + selection only on the open transition + const wasOpenRef = useRef(false); + useEffect(() => { + if (open && !wasOpenRef.current) { + wasOpenRef.current = true; + form.reset({ + sourceCamera: "", + targetMode: "new", + newName: "", + existingTargets: [], + }); + setSelectedCategories(getCategoryDefaults(true)); + } else if (!open) { + wasOpenRef.current = false; + } + }, [open, form]); + + // Drop the source camera from the target selection if it gets picked. + useEffect(() => { + if (!sourceCamera) return; + const current = form.getValues("existingTargets"); + if (current.includes(sourceCamera)) { + form.setValue( + "existingTargets", + current.filter((c) => c !== sourceCamera), + ); + } + }, [sourceCamera, form]); + + // Reset selection to per-mode defaults when the user switches target mode. + useEffect(() => { + setSelectedCategories(getCategoryDefaults(targetIsNew)); + }, [targetIsNew]); + + const toggleCategory = useCallback((key: CloneCategoryKey) => { + setSelectedCategories((prev) => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }, []); + + const selectAllCategories = useCallback(() => { + setSelectedCategories((prev) => { + const next = new Set(prev); + const includeSpatial = targetIsNew || allResMatch; + for (const cat of CLONE_CATEGORIES) { + if (cat.newCameraOnly && !targetIsNew) continue; + if (cat.group === "spatial" && !includeSpatial) continue; + if (cat.group === "streams") continue; + next.add(cat.key); + } + return next; + }); + }, [targetIsNew, allResMatch]); + + const selectNoneCategories = useCallback(() => { + setSelectedCategories((prev) => { + const next = new Set(); + for (const cat of CLONE_CATEGORIES) { + if (cat.group === "streams" && prev.has(cat.key)) { + next.add(cat.key); + } + } + return next; + }); + }, []); + + const visibleCategories = useMemo( + () => CLONE_CATEGORIES.filter((c) => targetIsNew || !c.newCameraOnly), + [targetIsNew], + ); + + const groupedCategories = useMemo(() => { + const groups: Record = { + general: [], + spatial: [], + streams: [], + }; + for (const c of visibleCategories) { + groups[c.group].push(c); + } + return groups; + }, [visibleCategories]); + + const sourceFriendlyName = + config?.cameras?.[sourceCamera]?.friendly_name ?? sourceCamera; + + const fullSchema = useConfigSchema(); + const { send: sendRestart } = useRestart(); + const statusBar = useContext(StatusBarMessagesContext); + const [restartDialogOpen, setRestartDialogOpen] = useState(false); + + const watchedNewName = + useWatch({ control: form.control, name: "newName" }) ?? ""; + + // Payloads grouped per destination camera. New mode has a single target; + // existing mode fans out across every selected camera. + const targetPayloads = useMemo< + { target: string; payloads: ReturnType }[] + >(() => { + if (!config || !fullSchema || !srcCfg) { + return []; + } + if (targetIsNew) { + const finalName = processCameraName(watchedNewName || "").finalCameraName; + if (!watchedNewName || !finalName) return []; + return [ + { + target: finalName, + payloads: buildClonedCameraPayloads({ + sourceCfg: srcCfg, + sourceName: sourceCamera, + targetInput: watchedNewName, + targetIsNew: true, + selectedKeys: selectedCategories, + fullConfig: config, + fullSchema, + rawPaths, + }), + }, + ]; + } + return existingTargets + .filter((cam) => config.cameras?.[cam]) + .map((cam) => ({ + target: cam, + payloads: buildClonedCameraPayloads({ + sourceCfg: srcCfg, + sourceName: sourceCamera, + targetInput: cam, + targetIsNew: false, + selectedKeys: selectedCategories, + fullConfig: config, + fullSchema, + rawPaths, + }), + })); + }, [ + config, + fullSchema, + srcCfg, + sourceCamera, + targetIsNew, + existingTargets, + watchedNewName, + selectedCategories, + rawPaths, + ]); + + const previewPayloads = useMemo( + () => targetPayloads.flatMap((tp) => tp.payloads), + [targetPayloads], + ); + + const previewItems = useMemo( + () => + targetPayloads.flatMap((tp) => + buildClonePreviewItems(tp.payloads, tp.target), + ), + [targetPayloads], + ); + + const anyNeedsRestart = previewPayloads.some((p) => p.needsRestart); + const changeCount = previewItems.length; + + const onSubmit = useCallback( + async (values: CloneFormValues) => { + if (!config || !srcCfg || !fullSchema) return; + if (previewPayloads.length === 0) { + toast.error( + t("cameraManagement.clone.toast.submitError", { + errorMessage: t("cameraManagement.clone.footer.changeCount", { + count: 0, + }), + }), + ); + return; + } + + const friendlyName = (cam: string) => + config.cameras?.[cam]?.friendly_name ?? cam; + + const extractError = (error: unknown) => + (axios.isAxiosError(error) && + (error.response?.data?.message || error.response?.data?.detail)) || + (error instanceof Error ? error.message : "Unknown error"); + + const restartAction = ( + setRestartDialogOpen(true)}> + + + ); + + const markRestartRequired = () => + statusBar?.addMessage( + "config_restart_required", + t("configForm.restartRequiredFooter"), + undefined, + "config_restart_required", + ); + + setIsSubmitting(true); + + if (targetIsNew) { + const targetLabel = values.newName.trim(); + const payloads = targetPayloads[0]?.payloads ?? []; + let appliedCount = 0; + let failedSection: string | undefined; + let failureMessage: string | undefined; + + try { + for (const payload of payloads) { + try { + await axios.put("config/set", { + requires_restart: payload.needsRestart ? 1 : 0, + update_topic: payload.updateTopic, + config_data: buildConfigDataForPath( + payload.basePath, + payload.sanitizedOverrides, + ), + }); + appliedCount += 1; + } catch (error) { + failedSection = payload.basePath; + failureMessage = extractError(error); + break; + } + } + } finally { + await swrMutate("config"); + setIsSubmitting(false); + } + + if (failedSection) { + toast.error( + appliedCount > 0 + ? t("cameraManagement.clone.toast.newCameraPartialFailure", { + cameraName: targetLabel, + errorMessage: failureMessage, + }) + : t("cameraManagement.clone.toast.partialFailure", { + successCount: appliedCount, + failedSection, + errorMessage: failureMessage, + }), + { position: "top-center" }, + ); + return; + } + + if (anyNeedsRestart) { + markRestartRequired(); + toast.success( + t("cameraManagement.clone.toast.successWithRestart", { + cameraName: targetLabel, + }), + { position: "top-center", duration: 10000, action: restartAction }, + ); + } else { + toast.success( + t("cameraManagement.clone.toast.success", { + cameraName: targetLabel, + }), + { position: "top-center" }, + ); + } + + onClose(); + return; + } + + // One or more existing cameras: keep going if a camera fails, summarize. + const succeeded: string[] = []; + const failed: string[] = []; + let lastError: string | undefined; + + try { + for (const { target, payloads } of targetPayloads) { + let cameraError: string | undefined; + for (const payload of payloads) { + try { + await axios.put("config/set", { + requires_restart: payload.needsRestart ? 1 : 0, + update_topic: payload.updateTopic, + config_data: buildConfigDataForPath( + payload.basePath, + payload.sanitizedOverrides, + ), + }); + } catch (error) { + cameraError = extractError(error); + break; + } + } + if (cameraError) { + failed.push(friendlyName(target)); + lastError = cameraError; + } else { + succeeded.push(friendlyName(target)); + } + } + } finally { + await swrMutate("config"); + setIsSubmitting(false); + } + + if (failed.length > 0) { + toast.error( + t("cameraManagement.clone.toast.partialFailureMulti", { + successCount: succeeded.length, + failed: failed.join(", "), + errorMessage: lastError, + }), + { position: "top-center", duration: 10000 }, + ); + return; + } + + const singleLabel = succeeded.length === 1 ? succeeded[0] : undefined; + + if (anyNeedsRestart) { + markRestartRequired(); + toast.success( + singleLabel + ? t("cameraManagement.clone.toast.successWithRestart", { + cameraName: singleLabel, + }) + : t("cameraManagement.clone.toast.successMultiWithRestart", { + count: succeeded.length, + }), + { position: "top-center", duration: 10000, action: restartAction }, + ); + } else { + toast.success( + singleLabel + ? t("cameraManagement.clone.toast.success", { + cameraName: singleLabel, + }) + : t("cameraManagement.clone.toast.successMulti", { + count: succeeded.length, + }), + { position: "top-center" }, + ); + } + + onClose(); + }, + [ + config, + srcCfg, + fullSchema, + previewPayloads, + targetPayloads, + targetIsNew, + anyNeedsRestart, + onClose, + statusBar, + t, + ], + ); + + return ( + !o && onClose()}> + e.preventDefault()} + > + + {t("cameraManagement.clone.title")} + + {t("cameraManagement.clone.description")} + + + +
    + +
    +
    + + ( + + + + + + + )} + /> +
    + +
    + + ( + + + +
    +
    + + +
    + {targetMode === "new" && ( + + + {t( + "cameraManagement.clone.target.newNameLabel", + )} + + + + + {form.formState.errors.newName?.message && ( +

    + {String( + form.formState.errors.newName.message, + )} +

    + )} +

    + {t( + "cameraManagement.clone.target.newStreamsForced", + )} +

    +
    + )} +
    +
    +
    + + +
    + {targetMode === "existing" && + otherCameras.length > 0 && ( + { + const selected = tgtField.value ?? []; + const allSelected = + otherCameras.length > 0 && + otherCameras.every((c) => + selected.includes(c), + ); + const selectedNames = otherCameras + .filter((c) => selected.includes(c)) + .map( + (c) => + config?.cameras?.[c]?.friendly_name ?? + c, + ); + const summary = allSelected + ? t( + "cameraManagement.clone.target.allCameras", + ) + : selectedNames.length > 0 + ? selectedNames.join(", ") + : t( + "cameraManagement.clone.target.existingPlaceholder", + ); + return ( + + + + + + + + +
    + + tgtField.onChange( + checked + ? [...otherCameras] + : [], + ) + } + /> +
    + {otherCameras.map((cam) => ( + + tgtField.onChange( + checked + ? [...selected, cam] + : selected.filter( + (c) => c !== cam, + ), + ) + } + /> + ))} +
    +
    +
    +
    + +
    + ); + }} + /> + )} +
    +
    +
    +
    + )} + /> +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    + +

    + {t("cameraManagement.clone.categories.description")} +

    +
    +
    + + {t("cameraManagement.clone.categories.selectAll")} + + + + {t("cameraManagement.clone.categories.selectNone")} + +
    +
    + +
    + +
    + {groupedCategories.general.map((cat) => ( + + ))} +
    +
    + + {groupedCategories.spatial.length > 0 && ( +
    + + {!targetIsNew && + srcCfg?.detect && + mismatchedTargets.length > 0 && ( + + + + {t( + "cameraManagement.clone.categories.spatialWarningTitle", + )} + + + {t( + "cameraManagement.clone.categories.spatialWarning", + { + srcCamera: sourceFriendlyName, + srcWidth: srcCfg.detect.width, + srcHeight: srcCfg.detect.height, + cameras: mismatchedTargets + .map( + (c) => + config?.cameras?.[c]?.friendly_name ?? c, + ) + .join(", "), + }, + )} + + + )} +
    + {groupedCategories.spatial.map((cat) => ( + + ))} +
    +
    + )} + + {targetIsNew && groupedCategories.streams.length > 0 && ( +
    + +
    + {groupedCategories.streams.map((cat) => ( + + ))} +
    +
    + )} +
    + + +
    + {changeCount > 0 && ( + <> +
    + + {t("cameraManagement.clone.footer.changeCount", { + count: changeCount, + })} + + {changeCount > 0 && ( + + )} +
    + + {anyNeedsRestart + ? t("cameraManagement.clone.footer.restartNeeded") + : t("cameraManagement.clone.footer.liveOnly")} + + + )} +
    +
    + + +
    +
    +
    + + setRestartDialogOpen(false)} + onRestart={() => sendRestart("restart")} + /> +
    +
    + ); +} diff --git a/web/src/utils/cameraClone.ts b/web/src/utils/cameraClone.ts new file mode 100644 index 0000000000..c985c917f0 --- /dev/null +++ b/web/src/utils/cameraClone.ts @@ -0,0 +1,856 @@ +import cloneDeep from "lodash/cloneDeep"; +import isEqual from "lodash/isEqual"; +import merge from "lodash/merge"; +import type { RJSFSchema } from "@rjsf/utils"; + +import { + buildOverrides, + cameraUpdateTopicMap, + flattenOverrides, + getEffectiveAttributeLabels, + getSectionConfig, + prepareSectionSavePayload, + resolveHiddenFieldEntries, + sanitizeSectionData, + type SectionSavePayload, +} from "@/utils/configUtil"; +import { applySchemaDefaults } from "@/lib/config-schema"; +import type { SaveAllPreviewItem } from "@/components/overlay/detail/SaveAllPreviewPopover"; +import type { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; +import type { + ConfigSectionData, + JsonObject, + JsonValue, +} from "@/types/configForm"; +import { processCameraName } from "@/utils/cameraUtil"; + +/** + * Sections whose `filters` dict is auto-populated by the backend at parse + * time. `attributeBump` reflects the global-level `min_score=0.7` override + * the backend applies to attribute labels (face, license_plate, Frigate+ + * couriers) — see `frigate/config/config.py`. + */ +const FILTER_SECTION_DEFS: Record< + string, + { + listField: string; + filterDef: string; + attributeBump?: { min_score: number }; + } +> = { + objects: { + listField: "track", + filterDef: "FilterConfig", + attributeBump: { min_score: 0.7 }, + }, + audio: { listField: "listen", filterDef: "AudioFilterConfig" }, +}; + +function resolveDef(schema: RJSFSchema, name: string): RJSFSchema | undefined { + const defs = + (schema as { $defs?: Record }).$defs ?? + (schema as { definitions?: Record }).definitions; + return defs ? defs[name] : undefined; +} + +/** + * Reduce each filter entry to the fields that differ from the backend's + * auto-default. An entry that is entirely auto-populated drops out; a + * partially-customized entry keeps only its customized fields, so cloning + * doesn't copy the auto-populated default for every other field. + */ +function stripAutoDefaultFilters( + section: string, + sourceSection: JsonObject, + fullSchema: RJSFSchema, + fullConfig: FrigateConfig, + fullCameraConfig: CameraConfig, +): JsonObject { + const def = FILTER_SECTION_DEFS[section]; + if (!def) return sourceSection; + const filters = sourceSection.filters; + if (!filters || typeof filters !== "object" || Array.isArray(filters)) { + return sourceSection; + } + const filterDef = resolveDef(fullSchema, def.filterDef); + if (!filterDef) return sourceSection; + const baseDefaults = applySchemaDefaults(filterDef, {}) as JsonObject; + const attributeDefaults = def.attributeBump + ? ({ ...baseDefaults, ...def.attributeBump } as JsonObject) + : baseDefaults; + const attributeSet = + section === "objects" + ? new Set( + getEffectiveAttributeLabels(fullConfig, fullCameraConfig, "camera"), + ) + : new Set(); + + // Ignore runtime-only `mask`/`raw_mask`: the API ships them as `{}` while the + // schema default omits them, which would otherwise break the equality check. + const withoutRuntimeFields = (entry: JsonValue): JsonValue => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return entry; + } + const copy = { ...(entry as JsonObject) }; + delete copy.mask; + delete copy.raw_mask; + return copy; + }; + + const cleaned: JsonObject = {}; + for (const [label, value] of Object.entries(filters as JsonObject)) { + const expected = attributeSet.has(label) ? attributeDefaults : baseDefaults; + const valNorm = withoutRuntimeFields(value as JsonValue); + const expNorm = withoutRuntimeFields(expected as JsonValue); + + // Non-object filter value: keep only if it differs from the default. + if ( + !valNorm || + typeof valNorm !== "object" || + Array.isArray(valNorm) || + !expNorm || + typeof expNorm !== "object" || + Array.isArray(expNorm) + ) { + if (!isEqual(valNorm, expNorm)) { + cleaned[label] = value as JsonValue; + } + continue; + } + + const diff: JsonObject = {}; + for (const [field, fieldValue] of Object.entries(valNorm as JsonObject)) { + if (!isEqual(fieldValue, (expNorm as JsonObject)[field])) { + diff[field] = fieldValue as JsonValue; + } + } + if (Object.keys(diff).length > 0) { + cleaned[label] = diff; + } + } + return { ...sourceSection, filters: cleaned }; +} + +/** + * Strip runtime-only fields from each entry of a dict-of-objects (mask + * `enabled_in_config`/`raw_coordinates`, zone `color`) that clone re-injects + * from the API. + */ +function stripDictEntryFields( + dict: unknown, + fieldsToStrip: readonly string[], +): unknown { + if (!dict || typeof dict !== "object" || Array.isArray(dict)) return dict; + const result: JsonObject = {}; + for (const [key, value] of Object.entries(dict as JsonObject)) { + if (value && typeof value === "object" && !Array.isArray(value)) { + const cleaned = { ...(value as JsonObject) }; + for (const field of fieldsToStrip) { + delete cleaned[field]; + } + result[key] = cleaned as JsonValue; + } else { + result[key] = value as JsonValue; + } + } + return result; +} + +/** + * Per-object masks (`objects.filters.
    -
    +
    diff --git a/web/src/components/classification/wizard/Step3ChooseExamples.tsx b/web/src/components/classification/wizard/Step3ChooseExamples.tsx index c6693d0296..75f4d263f0 100644 --- a/web/src/components/classification/wizard/Step3ChooseExamples.tsx +++ b/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -1,4 +1,4 @@ -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { useTranslation } from "react-i18next"; import { useState, useEffect, useCallback, useMemo } from "react"; import ActivityIndicator from "@/components/indicators/activity-indicator"; @@ -540,7 +540,7 @@ export default function Step3ChooseExamples({ {t("button.continue", { ns: "common" })} @@ -693,7 +693,7 @@ export default function Step3ChooseExamples({ )} {!isTraining && ( -
    +
    diff --git a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx index fb53055bcc..cf8b6778c6 100644 --- a/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx +++ b/web/src/components/config-form/sectionExtras/NotificationsSettingsExtras.tsx @@ -10,7 +10,6 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Toaster } from "@/components/ui/sonner"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { FrigateConfig } from "@/types/frigateConfig"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -491,7 +490,6 @@ export default function NotificationsSettingsExtras({ return (
    -
    {isAdmin && ( @@ -521,7 +519,7 @@ export default function NotificationsSettingsExtras({ diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index b3261a5cc4..685b06ebe3 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -32,7 +32,7 @@ import { ProfileOverridesBadge } from "./ProfileOverridesBadge"; import { useSectionSchema } from "@/hooks/use-config-schema"; import type { FrigateConfig } from "@/types/frigateConfig"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { LuChevronDown, LuChevronRight } from "react-icons/lu"; import Heading from "@/components/ui/heading"; import get from "lodash/get"; @@ -1236,7 +1236,7 @@ export function ConfigSection({ {t("button.cancel", { ns: "common" })} { onDeleteProfileSection?.(); setIsDeleteProfileDialogOpen(false); diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index bdd61eb1ff..9d703b3c8d 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -371,7 +371,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { key={group.groupKey} className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4" > -
    +
    {group.label}
    diff --git a/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx b/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx index 21bba78cca..2f32de6453 100644 --- a/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx +++ b/web/src/components/config-form/theme/widgets/ArrayAsTextWidget.tsx @@ -79,7 +79,7 @@ export function ArrayAsTextWidget(props: WidgetProps) { return (