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")}