diff --git a/docs/data/object_detectors_models.yaml b/docs/data/object_detectors_models.yaml
index b684d59ace..b50a5992b7 100644
--- a/docs/data/object_detectors_models.yaml
+++ b/docs/data/object_detectors_models.yaml
@@ -36,13 +36,13 @@ edgeTPU:
height: 320 # <--- should match the imgsize of the model, typically 320
path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
labelmap_path: /config/labels-coco17.txt
-hailo8l:
- title: Hailo-8/Hailo-8L
+hailo:
+ title: Hailo
models:
- key: yolo
label: YOLO
recommended: true
- download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup based on the detected hardware. Once cached under `/config/model_cache/hailo`, the model works fully offline.
+ download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup, choosing the build that matches the attached device. Once cached under `/config/model_cache`, the model works fully offline.
ui: |-
Navigate to **Settings > System > Detection models** and select **Hailo** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure the model settings:
@@ -60,7 +60,7 @@ hailo8l:
yaml: |-
models:
- devices:
- - hailo8l:PCIe
+ - hailo:PCIe
width: 320
height: 320
input_tensor: nhwc
@@ -101,7 +101,7 @@ hailo8l:
yaml: |-
models:
- devices:
- - hailo8l:PCIe
+ - hailo:PCIe
width: 300
height: 300
input_tensor: nhwc
diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md
index 3a65a1f89a..cd2ca1fdfb 100644
--- a/docs/docs/configuration/object_detectors.md
+++ b/docs/docs/configuration/object_detectors.md
@@ -22,7 +22,7 @@ Frigate supports multiple different detectors that work on different types of ha
**Most Hardware**
- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB, Mini PCIe, and m.2 formats allowing for a wide range of compatibility with devices.
-- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
+- [Hailo](#hailo): The Hailo-8, Hailo-8L and Hailo-8R AI Acceleration modules are available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
- [MemryX](#memryx-mx3): The MX3 Acceleration module is available in m.2 format, offering broad compatibility across various platforms.
**AMD**
@@ -285,9 +285,9 @@ models:
---
-## Hailo-8
+## Hailo
-This detector is available for use with both Hailo-8 and Hailo-8L AI Acceleration Modules. The integration automatically detects your hardware architecture via the Hailo CLI and selects the appropriate default model if no custom model is specified.
+This detector is available for use with the Hailo-8, Hailo-8L and Hailo-8R AI Acceleration Modules. The integration identifies which of them is attached and selects the matching default model if no custom model is specified.
See the [installation docs](../frigate/installation.md#hailo-8) for information on configuring the Hailo hardware.
@@ -308,11 +308,11 @@ The HailoRT runtime is not part of the Frigate image. It is downloaded and insta
When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**.
If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`.
-
+
For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo
-Hailo8 supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation.
+Hailo supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation.
> **Note:**
> The config.path parameter can accept either a local file path or a URL ending with .hef. When provided, the detector will first check if the path is a local file path. If the file exists locally, it will use it directly. If the file is not found locally or if a URL was provided, it will attempt to download the model from the specified URL.
diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md
index 1df3bb8368..3301eb5d42 100644
--- a/docs/docs/frigate/hardware.md
+++ b/docs/docs/frigate/hardware.md
@@ -54,7 +54,7 @@ Frigate supports multiple different detectors that work on different types of ha
**Most Hardware**
-- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices.
+- [Hailo](#hailo-8): The Hailo-8, Hailo-8L and Hailo-8R AI Acceleration modules are available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices.
- [Supports many model architectures](../../configuration/object_detectors#configuration-hailo)
- Runs best with tiny or small size models
@@ -111,12 +111,13 @@ Frigate supports multiple different detectors that work on different types of ha
### Hailo-8
-Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
+Frigate supports the Hailo-8, Hailo-8L and Hailo-8R AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate identifies which of them is attached and selects the matching default model when a custom model isn’t provided.
**Default Model Configuration:**
-- **Hailo-8L:** Default model is **YOLOv6n**.
-- **Hailo-8:** Default model is **YOLOv6n**.
+- **Hailo-8L:** Default model is **YOLOv6n**, compiled for the Hailo-8L.
+- **Hailo-8:** Default model is **YOLOv6n**, compiled for the Hailo-8.
+- **Hailo-8R:** Default model is the **Hailo-8** build of **YOLOv6n**, since the Hailo Model Zoo publishes no Hailo-8R build.
In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup.
diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md
index 880292f232..239cf4290a 100644
--- a/docs/docs/frigate/installation.md
+++ b/docs/docs/frigate/installation.md
@@ -122,7 +122,7 @@ Additionally, the USB Coral draws a considerable amount of power. If using any o
### Hailo-8
-The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments.
+The Hailo-8, Hailo-8L and Hailo-8R AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments.
The HailoRT runtime is not part of the Frigate image; Frigate downloads and installs it at first start once a Hailo detector is configured. Containers without internet access can provide the files themselves, see [Detector runtimes](/frigate/network_requirements#detector-runtimes).
@@ -300,7 +300,7 @@ If you are using `docker run`, add this option to your command `--device /dev/ha
#### Configuration
-Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8) to complete the setup.
+Finally, configure [hardware object detection](/configuration/object_detectors#hailo) to complete the setup.
### MemryX MX3
diff --git a/docs/docs/frigate/network_requirements.md b/docs/docs/frigate/network_requirements.md
index ab3df1e451..2741e14504 100644
--- a/docs/docs/frigate/network_requirements.md
+++ b/docs/docs/frigate/network_requirements.md
@@ -47,7 +47,7 @@ If you are using one of the following hardware detectors and have not provided y
| Detector | Model Downloaded | Source |
| ------------------------------------------------------------------ | -------------------- | ------------------------ |
| [Rockchip RKNN](/configuration/object_detectors#rockchip-platform) | RKNN detection model | GitHub |
-| [Hailo 8 / 8L](/configuration/object_detectors#hailo-8) | YOLOv6n (.hef) | Hailo Model Zoo (AWS S3) |
+| [Hailo 8 / 8L / 8R](/configuration/object_detectors#hailo) | YOLOv6n (.hef) | Hailo Model Zoo (AWS S3) |
| [AXERA AXEngine](/configuration/object_detectors) | Detection model | HuggingFace |
:::note
@@ -60,16 +60,16 @@ The default CPU, EdgeTPU, and OpenVINO object detection models are bundled into
The SDKs for a few hardware detectors are not shipped in the Frigate image. They are downloaded the first time that detector is configured, verified against checksums pinned in the Frigate release, and installed into the Frigate user's home directory (`/config/.local` by default). Once installed they are not downloaded again until a Frigate release pins a new version.
-| Detector | Version | Files | Source |
-| -------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
-| [Hailo 8 / 8L](/configuration/object_detectors#hailo-8) | 4.21.0 | `hailort-debian12-amd64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_x86_64.whl` on x86, `hailort-debian12-arm64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_aarch64.whl` on arm64 | [GitHub release](https://github.com/frigate-nvr/hailort/releases/tag/v4.21.0) |
-| [MemryX MX3](/configuration/object_detectors#memryx-mx3) | 2.1.0 | `mx_accl_frigate-2.1.0.zip` (the release source archive, renamed) | [GitHub archive](https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip) |
-| [AXERA AXEngine](/configuration/object_detectors#axera) | 0.1.3 | `axengine-0.1.3-py3-none-any.whl` | [GitHub release](https://github.com/AXERA-TECH/pyaxengine/releases/tag/0.1.3-frigate) |
+| Detector | Version | Files | Source |
+| ---------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| [Hailo 8 / 8L / 8R](/configuration/object_detectors#hailo) | 4.21.0 | `hailort-debian12-amd64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_x86_64.whl` on x86, `hailort-debian12-arm64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_aarch64.whl` on arm64 | [GitHub release](https://github.com/frigate-nvr/hailort/releases/tag/v4.21.0) |
+| [MemryX MX3](/configuration/object_detectors#memryx-mx3) | 2.1.0 | `mx_accl_frigate-2.1.0.zip` (the release source archive, renamed) | [GitHub archive](https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip) |
+| [AXERA AXEngine](/configuration/object_detectors#axera) | 0.1.3 | `axengine-0.1.3-py3-none-any.whl` | [GitHub release](https://github.com/AXERA-TECH/pyaxengine/releases/tag/0.1.3-frigate) |
If the container cannot reach GitHub, provide the files yourself:
1. Download the files for your architecture on a machine with internet access.
-2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes//`, where `` is the detector `type` from your config (`hailo8l`, `memryx`, or `axengine`).
+2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes//`, where `` is the detector named in your config's `devices` (`hailo`, `memryx`, or `axengine`).
3. Start Frigate. Files whose checksum matches are installed without any download; a file with the wrong checksum is discarded and downloaded again, so a failed startup log names the file to replace.
The `GITHUB_ENDPOINT` mirror variable below applies to these downloads as well.
diff --git a/docs/docs/plus/index.md b/docs/docs/plus/index.md
index 76792f96f0..d86d6ecc60 100644
--- a/docs/docs/plus/index.md
+++ b/docs/docs/plus/index.md
@@ -41,7 +41,7 @@ Rockchip models are automatically converted as of 0.17. For 0.16, YOLOv9 onnx mo
## Supported detector types
-Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo8l`), and Rockchip (`rknn`) detectors.
+Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo`), and Rockchip (`rknn`) detectors.
| Hardware | Recommended Detector Type | Recommended Model Type |
| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- |
@@ -50,7 +50,7 @@ Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVi
| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` |
| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` |
| [AMD ROCm GPU](/configuration/object_detectors#amdrocm-gpu-detector) | `onnx` | `yolov9` |
-| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo-8) | `hailo8l` | `yolov9` |
+| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo) | `hailo` | `yolov9` |
| [Rockchip NPU](/configuration/object_detectors#rockchip-platform) | `rknn` | `yolov9` |
## Improving your model
diff --git a/frigate/api/app.py b/frigate/api/app.py
index 73a2fe4b56..4078ebf082 100644
--- a/frigate/api/app.py
+++ b/frigate/api/app.py
@@ -2,7 +2,6 @@
import asyncio
import copy
-import json
import logging
import os
import platform
@@ -11,7 +10,6 @@ import urllib
from datetime import datetime, timedelta
from functools import reduce
from io import StringIO
-from pathlib import Path as FilePath
from typing import Any
import aiofiles
@@ -56,6 +54,7 @@ from frigate.jobs.media_sync import (
start_media_sync_job,
)
from frigate.models import Event, Timeline
+from frigate.plus import load_plus_model_info
from frigate.stats.prometheus import get_metrics, update_metrics
from frigate.types import JobStatusTypesEnum
from frigate.util.builtin import (
@@ -401,13 +400,7 @@ def config(request: Request):
model_dict["plus"] = None
if model.path:
- model_json_path = FilePath(model.path).with_suffix(".json")
-
- try:
- with open(model_json_path) as f:
- model_dict["plus"] = json.load(f)
- except (FileNotFoundError, json.JSONDecodeError):
- pass
+ model_dict["plus"] = load_plus_model_info(os.path.basename(model.path))
return JSONResponse(content=config)
diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py
index 415b9389e2..1193ba0740 100644
--- a/frigate/detectors/detector_config.py
+++ b/frigate/detectors/detector_config.py
@@ -3,14 +3,14 @@ import json
import logging
import os
from enum import Enum
-from typing import Any, ClassVar
+from typing import ClassVar
import requests
from pydantic import BaseModel, ConfigDict, Field
from pydantic.fields import PrivateAttr
from frigate.const import DEFAULT_ATTRIBUTE_LABEL_MAP, MODEL_CACHE_DIR
-from frigate.plus import PlusApi
+from frigate.plus import PlusApi, load_plus_model_info
from frigate.util.builtin import generate_color_palette, load_labels
logger = logging.getLogger(__name__)
@@ -190,12 +190,13 @@ class ModelConfig(BaseModel):
# download the model info if it doesn't exist
if not os.path.isfile(model_info_path):
- model_info = plus_api.get_model_info(model_id)
with open(model_info_path, "w") as f:
- json.dump(model_info, f)
- else:
- with open(model_info_path) as f:
- model_info: dict[str, Any] = json.load(f)
+ json.dump(plus_api.get_model_info(model_id), f)
+
+ model_info = load_plus_model_info(model_id)
+
+ if model_info is None:
+ raise ValueError(f"Unable to read the model info for {model_id}")
if detector and detector not in model_info["supportedDetectors"]:
raise ValueError(f"Model does not support detector type of {detector}")
diff --git a/frigate/detectors/hardware.py b/frigate/detectors/hardware.py
index 756fbd430e..37b1f48e8e 100644
--- a/frigate/detectors/hardware.py
+++ b/frigate/detectors/hardware.py
@@ -259,8 +259,8 @@ def detect_hailo() -> DetectionHardware | None:
# the hailo runtime schedules across every attached device itself, so there
# is nothing to address individually
- units = [HardwareUnit(device="hailo8l:PCIe", label=os.path.basename(nodes[0]))]
- return _hardware("hailo8l", "hailo8l", "Hailo", units)
+ units = [HardwareUnit(device="hailo:PCIe", label=os.path.basename(nodes[0]))]
+ return _hardware("hailo", "hailo", "Hailo", units)
def detect_memryx() -> DetectionHardware | None:
diff --git a/frigate/detectors/plugins/hailo8l.py b/frigate/detectors/plugins/hailo.py
similarity index 99%
rename from frigate/detectors/plugins/hailo8l.py
rename to frigate/detectors/plugins/hailo.py
index 0e1c480f37..a9c3fc7738 100755
--- a/frigate/detectors/plugins/hailo8l.py
+++ b/frigate/detectors/plugins/hailo.py
@@ -53,7 +53,7 @@ def preprocess_tensor(image: np.ndarray, model_w: int, model_h: int) -> np.ndarr
# ----------------- Global Constants ----------------- #
-DETECTOR_KEY = "hailo8l"
+DETECTOR_KEY = "hailo"
ARCH = None
H8_DEFAULT_MODEL = "yolov6n.hef"
H8L_DEFAULT_MODEL = "yolov6n.hef"
@@ -469,10 +469,10 @@ class HailoDetector(DetectionApi):
# ----------------- HailoDetectorConfig Class ----------------- #
class HailoDetectorConfig(BaseDetectorConfig):
- """Hailo-8/Hailo-8L detector using HEF models and the HailoRT SDK for inference on Hailo hardware."""
+ """Hailo detector using HEF models and the HailoRT SDK for inference on Hailo hardware."""
model_config = ConfigDict(
- title="Hailo-8/Hailo-8L",
+ title="Hailo",
)
type: Literal[DETECTOR_KEY]
diff --git a/frigate/plus.py b/frigate/plus.py
index d528aa1757..5cbdeda5dc 100644
--- a/frigate/plus.py
+++ b/frigate/plus.py
@@ -11,11 +11,50 @@ import requests
from numpy import ndarray
from requests.models import Response
-from frigate.const import PLUS_API_HOST, PLUS_ENV_VAR
+from frigate.const import MODEL_CACHE_DIR, PLUS_API_HOST, PLUS_ENV_VAR
logger = logging.getLogger(__name__)
+def add_hailo_alias(model_info: dict[str, Any]) -> dict[str, Any]:
+ """Name the hailo detector by its current key as well as its old one.
+
+ Frigate+ reports every Hailo model as supporting hailo8l, which this
+ detector was called before it was renamed to cover every Hailo device.
+ The old key is kept so an older Frigate still matches the model.
+
+ Args:
+ model_info: A Frigate+ model's metadata, edited in place
+
+ Returns:
+ The same metadata
+ """
+ supported = model_info.get("supportedDetectors")
+
+ if supported and "hailo8l" in supported and "hailo" not in supported:
+ supported.append("hailo")
+
+ return model_info
+
+
+def load_plus_model_info(model_id: str) -> dict[str, Any] | None:
+ """Read a Frigate+ model's cached info file.
+
+ Args:
+ model_id: The Frigate+ model id
+
+ Returns:
+ The model info, or None when it has not been cached or cannot be read
+ """
+ try:
+ with open(os.path.join(MODEL_CACHE_DIR, f"{model_id}.json")) as f:
+ model_info: dict[str, Any] = json.load(f)
+ except (OSError, ValueError):
+ return None
+
+ return add_hailo_alias(model_info)
+
+
def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
if image.shape[1] >= image.shape[0]:
width = min(max_dim, image.shape[1])
@@ -241,4 +280,9 @@ class PlusApi:
if not r.ok:
raise Exception(r.text)
- return r.json()
+ models = r.json()
+
+ for model in models.get("list") or []:
+ add_hailo_alias(model)
+
+ return models
diff --git a/frigate/stats/hardware.py b/frigate/stats/hardware.py
index f94e9de2e1..b8a877761c 100644
--- a/frigate/stats/hardware.py
+++ b/frigate/stats/hardware.py
@@ -467,7 +467,7 @@ def get_hardware_temperatures(detector_type: str) -> list[float | None]:
read_temperature(os.path.join(base, apex, "temp"))
for apex in sorted(os.listdir(base))
]
- elif detector_type == "hailo8l":
+ elif detector_type == "hailo":
hailo_temps = get_hailo_temps()
return [hailo_temps[name] for name in sorted(hailo_temps.keys())]
diff --git a/frigate/test/test_config_migration.py b/frigate/test/test_config_migration.py
index 8082ea18a5..f67cd5d252 100644
--- a/frigate/test/test_config_migration.py
+++ b/frigate/test/test_config_migration.py
@@ -12,6 +12,7 @@ from frigate.util.config import (
CURRENT_CONFIG_VERSION,
migrate_frigate_config,
migrate_models,
+ rename_hailo_detector,
)
@@ -164,6 +165,61 @@ class TestMigrateModels(unittest.TestCase):
self.assertEqual(migrated["mqtt"], {"host": "mqtt"})
+class TestMigrateRenamedDetectors(unittest.TestCase):
+ def test_a_hailo_detector_is_renamed(self):
+ migrated = migrate_models(
+ {"detectors": {"hailo": {"type": "hailo8l", "device": "PCIe"}}}
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo:PCIe"])
+
+ def test_a_hailo_detector_without_a_device(self):
+ migrated = migrate_models({"detectors": {"hailo": {"type": "hailo8l"}}})
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo"])
+
+ def test_two_hailo_detectors_stay_separate(self):
+ # the shareable lookup has to resolve through the renamed key
+ migrated = migrate_models(
+ {
+ "detectors": {
+ "hailo1": {"type": "hailo8l", "device": "PCIe"},
+ "hailo2": {"type": "hailo8l", "device": "PCIe"},
+ }
+ }
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo:PCIe", "hailo:PCIe"])
+
+
+class TestRenameHailoDetector(unittest.TestCase):
+ def test_a_renamed_detector_is_updated(self):
+ migrated = rename_hailo_detector(
+ {"models": [{"devices": ["hailo8l:PCIe", "hailo8l"]}]}
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo:PCIe", "hailo"])
+
+ def test_other_detectors_are_untouched(self):
+ migrated = rename_hailo_detector(
+ {"models": [{"devices": ["openvino:GPU", "cpu"]}]}
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["openvino:GPU", "cpu"])
+
+ def test_renaming_is_idempotent(self):
+ once = rename_hailo_detector({"models": [{"devices": ["hailo8l:PCIe"]}]})
+ twice = rename_hailo_detector(once)
+
+ self.assertEqual(twice["models"][0]["devices"], ["hailo:PCIe"])
+
+ def test_a_config_without_models_is_left_alone(self):
+ self.assertEqual(
+ rename_hailo_detector({"mqtt": {"enabled": False}}),
+ {"mqtt": {"enabled": False}},
+ )
+
+
class TestMigrateConfigFile(unittest.TestCase):
"""The full file migration, which is gated on shape as well as version."""
@@ -220,6 +276,37 @@ class TestMigrateConfigFile(unittest.TestCase):
os.path.exists(os.path.join(self.temp_dir.name, "backup_config.yaml"))
)
+ def test_a_legacy_hailo_detector_lands_on_the_hailo_key(self):
+ # the detectors key is folded into models after the version chain has
+ # run, so the rename has to happen there too
+ migrated = self._migrate(
+ "mqtt:\n"
+ " enabled: false\n"
+ "detectors:\n"
+ " hailo:\n"
+ " type: hailo8l\n"
+ " device: PCIe\n"
+ "cameras: {}\n"
+ "version: 0.18-0\n"
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo:PCIe"])
+ self.assertNotIn("detectors", migrated)
+
+ def test_a_hailo8l_device_is_renamed_at_the_current_version(self):
+ migrated = self._migrate(
+ "mqtt:\n"
+ " enabled: false\n"
+ "models:\n"
+ " - scene: all\n"
+ " devices:\n"
+ " - hailo8l:PCIe\n"
+ "cameras: {}\n"
+ f"version: {CURRENT_CONFIG_VERSION}\n"
+ )
+
+ self.assertEqual(migrated["models"][0]["devices"], ["hailo:PCIe"])
+
if __name__ == "__main__":
unittest.main(verbosity=2)
diff --git a/frigate/test/test_detector_hardware.py b/frigate/test/test_detector_hardware.py
index f7b0d8c78b..e3d0c87fd7 100644
--- a/frigate/test/test_detector_hardware.py
+++ b/frigate/test/test_detector_hardware.py
@@ -170,7 +170,7 @@ class TestAccelerators(HardwareProbeTestCase):
def test_hailo_is_found_by_its_device_node(self):
write(os.path.join(self.dev_root, "hailo0"))
- self.assertEqual(self.probe()["hailo8l"].units[0].device, "hailo8l:PCIe")
+ self.assertEqual(self.probe()["hailo"].units[0].device, "hailo:PCIe")
def test_each_memryx_node_is_a_unit(self):
write(os.path.join(self.dev_root, "memx0"))
diff --git a/frigate/test/test_hardware_stats.py b/frigate/test/test_hardware_stats.py
index 4069455238..2bb1e9cc9b 100644
--- a/frigate/test/test_hardware_stats.py
+++ b/frigate/test/test_hardware_stats.py
@@ -200,7 +200,7 @@ class TestScanDetectors(HardwareStatsTestCase):
self.scan([DeviceSpec("openvino:CPU", "openvino", "CPU")]), set()
)
self.assertEqual(self.scan([DeviceSpec("cpu", "cpu", None)]), set())
- self.assertEqual(self.scan([DeviceSpec("hailo8l", "hailo8l", None)]), set())
+ self.assertEqual(self.scan([DeviceSpec("hailo", "hailo", None)]), set())
def test_onnx_resolves_to_present_gpu(self):
spec = DeviceSpec("onnx", "onnx", None)
@@ -412,7 +412,7 @@ class TestHardwareTemperatures(unittest.TestCase):
return_value={"hailo8l-1": 52.0, "hailo8l-0": 51.0},
)
def test_hailo_sorted_by_name(self, temps):
- self.assertEqual(get_hardware_temperatures("hailo8l"), [51.0, 52.0])
+ self.assertEqual(get_hardware_temperatures("hailo"), [51.0, 52.0])
def test_unsupported_type(self):
self.assertEqual(get_hardware_temperatures("rknn"), [])
diff --git a/frigate/test/test_plus.py b/frigate/test/test_plus.py
new file mode 100644
index 0000000000..33b291640d
--- /dev/null
+++ b/frigate/test/test_plus.py
@@ -0,0 +1,79 @@
+"""Tests for Frigate+ model metadata."""
+
+import json
+import os
+import tempfile
+import unittest
+from unittest.mock import MagicMock, patch
+
+from frigate.plus import PlusApi, load_plus_model_info
+
+
+class TestHailoAlias(unittest.TestCase):
+ """Frigate+ reports Hailo models by the detector's pre-rename key.
+
+ Both the model list and a cached info file have to carry the current key,
+ or every Hailo model reads as unsupported.
+ """
+
+ def _get_models(self, models: list[dict]) -> list[dict]:
+ api = PlusApi.__new__(PlusApi)
+ response = MagicMock(ok=True, json=lambda: {"list": models})
+
+ with patch.object(PlusApi, "_get", return_value=response):
+ return api.get_models()["list"]
+
+ def test_the_model_list_gains_the_current_key(self):
+ models = self._get_models([{"supportedDetectors": ["hailo8l"]}])
+
+ self.assertEqual(models[0]["supportedDetectors"], ["hailo8l", "hailo"])
+
+ def test_the_old_key_is_kept_for_older_versions(self):
+ models = self._get_models([{"supportedDetectors": ["hailo8l"]}])
+
+ self.assertIn("hailo8l", models[0]["supportedDetectors"])
+
+ def test_other_detectors_are_untouched(self):
+ models = self._get_models([{"supportedDetectors": ["openvino", "onnx"]}])
+
+ self.assertEqual(models[0]["supportedDetectors"], ["openvino", "onnx"])
+
+ def test_a_model_already_naming_both_is_unchanged(self):
+ models = self._get_models([{"supportedDetectors": ["hailo8l", "hailo"]}])
+
+ self.assertEqual(models[0]["supportedDetectors"], ["hailo8l", "hailo"])
+
+ def test_an_empty_list(self):
+ self.assertEqual(self._get_models([]), [])
+
+
+class TestLoadPlusModelInfo(unittest.TestCase):
+ def setUp(self):
+ self.cache = tempfile.TemporaryDirectory()
+ self.addCleanup(self.cache.cleanup)
+ patcher = patch("frigate.plus.MODEL_CACHE_DIR", self.cache.name)
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ def _write(self, model_id: str, content: str) -> None:
+ with open(os.path.join(self.cache.name, f"{model_id}.json"), "w") as f:
+ f.write(content)
+
+ def test_a_cached_hailo_model_gains_the_current_key(self):
+ self._write("abc", json.dumps({"supportedDetectors": ["hailo8l"]}))
+
+ self.assertEqual(
+ load_plus_model_info("abc")["supportedDetectors"], ["hailo8l", "hailo"]
+ )
+
+ def test_a_missing_file(self):
+ self.assertIsNone(load_plus_model_info("nope"))
+
+ def test_an_unreadable_file(self):
+ self._write("bad", "{not json")
+
+ self.assertIsNone(load_plus_model_info("bad"))
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/frigate/util/config.py b/frigate/util/config.py
index 3a30a91f7b..7aa1d4f92e 100644
--- a/frigate/util/config.py
+++ b/frigate/util/config.py
@@ -168,7 +168,20 @@ def migrate_frigate_config(config_file: str):
# version and still use the pre-models detectors and model keys
needs_models = "detectors" in config or "model" in config
- if previous_version == CURRENT_CONFIG_VERSION and not needs_models:
+ # likewise, it may already be on the models list and still name the hailo
+ # detector by its old key
+ needs_detector_rename = any(
+ isinstance(device, str) and device.partition(":")[0] == "hailo8l"
+ for model in (config.get("models") or [])
+ if isinstance(model, dict)
+ for device in (model.get("devices") or [])
+ )
+
+ if (
+ previous_version == CURRENT_CONFIG_VERSION
+ and not needs_models
+ and not needs_detector_rename
+ ):
logger.info("frigate config does not need migration...")
return
@@ -242,6 +255,12 @@ def migrate_frigate_config(config_file: str):
with open(config_file, "w") as f:
yaml.dump(new_config, f)
+ if needs_detector_rename:
+ logger.info("Migrating renamed frigate detectors...")
+ new_config = rename_hailo_detector(new_config)
+ with open(config_file, "w") as f:
+ yaml.dump(new_config, f)
+
logger.info("Finished frigate config migration...")
@@ -775,9 +794,44 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]
return new_config
+def rename_hailo_detector(
+ config: dict[str, dict[str, Any]],
+) -> dict[str, dict[str, Any]]:
+ """Rename the hailo8l detector, which drives every Hailo device.
+
+ Args:
+ config: The loaded config
+
+ Returns:
+ The config with every models entry naming the detector 'hailo'
+ """
+ new_config = config.copy()
+
+ for model in new_config.get("models") or []:
+ if not isinstance(model, dict):
+ continue
+
+ devices = model.get("devices")
+
+ if not isinstance(devices, list):
+ continue
+
+ # assigned per index so ruamel keeps the comments on the list
+ for index, device in enumerate(devices):
+ if not isinstance(device, str):
+ continue
+
+ detector, separator, rest = device.partition(":")
+
+ if detector == "hailo8l":
+ devices[index] = f"hailo{separator}{rest}"
+
+ return new_config
+
+
def migrate_019_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]:
"""Handle migrating Frigate config to 0.19-0."""
- new_config = config.copy()
+ new_config = rename_hailo_detector(config)
_migrate_birdseye_mode(new_config.get("birdseye"))
@@ -822,6 +876,11 @@ def migrate_models(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any
detector = detector or {}
detector_type = detector.get("type", "cpu")
device = detector.get(DETECTOR_DEVICE_FIELDS.get(detector_type, "device"))
+
+ # hailo8l named one device, but the detector drives every Hailo device
+ if detector_type == "hailo8l":
+ detector_type = "hailo"
+
device_string = detector_type if device is None else f"{detector_type}:{device}"
# repeating a device now means running an extra inference process on it,
diff --git a/web/e2e/specs/settings/detection-models.spec.ts b/web/e2e/specs/settings/detection-models.spec.ts
index 188939dd2e..f764b70d7c 100644
--- a/web/e2e/specs/settings/detection-models.spec.ts
+++ b/web/e2e/specs/settings/detection-models.spec.ts
@@ -49,9 +49,43 @@ const PLUS_MODEL = {
height: 320,
};
+// Frigate+ builds a Hailo model per device and names the detector by its
+// pre-rename key; /api/plus/models adds the current one before serving it
+const HAILO_PLUS_MODELS = [
+ {
+ ...PLUS_MODEL,
+ id: "hailo8l1",
+ supportedDetectors: ["hailo8l", "hailo"],
+ hailoDevice: "hailo8l",
+ },
+ {
+ ...PLUS_MODEL,
+ id: "hailo8r1",
+ supportedDetectors: ["hailo8l", "hailo"],
+ hailoDevice: "hailo8r",
+ },
+];
+
+const HAILO_HARDWARE = [
+ {
+ key: "hailo",
+ detector: "hailo",
+ name: "Hailo",
+ units: [{ device: "hailo:PCIe", label: "hailo0" }],
+ count: 1,
+ unlimited: true,
+ },
+];
+
type SavedConfig = { config_data?: { models?: Model[] } };
-async function installRoutes(page: Page, models: Model[], plusEnabled = false) {
+async function installRoutes(
+ page: Page,
+ models: Model[],
+ plusEnabled = false,
+ plusModels: unknown[] = [PLUS_MODEL],
+ hailoHardware = false,
+) {
const config = configFactory({
models,
plus: { enabled: plusEnabled },
@@ -70,8 +104,14 @@ async function installRoutes(page: Page, models: Model[], plusEnabled = false) {
route.fulfill({ json: { models } }),
);
await page.route("**/api/plus/models", (route) =>
- route.fulfill({ json: [PLUS_MODEL] }),
+ route.fulfill({ json: plusModels }),
);
+
+ if (hailoHardware) {
+ await page.route("**/api/hardware/probe**", (route) =>
+ route.fulfill({ json: HAILO_HARDWARE }),
+ );
+ }
await page.route("**/api/config/set", async (route) => {
saves.push(route.request().postDataJSON() as SavedConfig);
await route.fulfill({ json: { success: true, require_restart: false } });
@@ -262,6 +302,35 @@ test.describe("Detection models settings @high", () => {
expect(saves.at(-1)?.config_data?.models?.[0].path).toBe("plus://abc123");
});
+ test("a Frigate+ Hailo model is listed by the device it was built for", async ({
+ frigateApp,
+ }) => {
+ // every Hailo model supports the one hailo detector, so the detector name
+ // says nothing; which device it was built for is what the user picks on
+ await installRoutes(
+ frigateApp.page,
+ [{ scene: "all", devices: ["hailo:PCIe"], path: "/config/custom.hef" }],
+ true,
+ HAILO_PLUS_MODELS,
+ true,
+ );
+ await openPage(frigateApp);
+
+ await frigateApp.page.getByRole("tab", { name: "Frigate+" }).click();
+ await frigateApp.page.getByRole("combobox").last().click();
+
+ const options = frigateApp.page.getByRole("option");
+
+ await expect(options).toHaveCount(2);
+ await expect(options.first()).toContainText("hailo8l");
+ await expect(options.last()).toContainText("hailo8r");
+
+ // knowing which device is attached is left to the user, so neither is
+ // ruled out here
+ await expect(options.first()).not.toHaveAttribute("aria-disabled", "true");
+ await expect(options.last()).not.toHaveAttribute("aria-disabled", "true");
+ });
+
test("a freshly opened page is not reported as modified", async ({
frigateApp,
}) => {
diff --git a/web/e2e/specs/system-health.spec.ts b/web/e2e/specs/system-health.spec.ts
index 94e96f106b..cc415a39cf 100644
--- a/web/e2e/specs/system-health.spec.ts
+++ b/web/e2e/specs/system-health.spec.ts
@@ -422,7 +422,7 @@ test.describe("System — Health hardware pane @medium", () => {
frigateApp,
}) => {
await frigateApp.installDefaults({
- config: { models: [{ scene: "all", devices: ["hailo8l"] }] },
+ config: { models: [{ scene: "all", devices: ["hailo"] }] },
stats: QUIET_STATS,
});
await frigateApp.goto("/system#health");
@@ -431,7 +431,7 @@ test.describe("System — Health hardware pane @medium", () => {
await expect(row).toHaveAttribute("data-state", "error", {
timeout: 15_000,
});
- await expect(row).toContainText("hailo8l was not found on this system");
+ await expect(row).toContainText("hailo was not found on this system");
});
test("a generic device the probe cannot enumerate is judged by its runtime", async ({
diff --git a/web/src/components/config-form/theme/fields/ModelSourcePicker.tsx b/web/src/components/config-form/theme/fields/ModelSourcePicker.tsx
index 8240a957f3..8aa60fb468 100644
--- a/web/src/components/config-form/theme/fields/ModelSourcePicker.tsx
+++ b/web/src/components/config-form/theme/fields/ModelSourcePicker.tsx
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { FrigateConfig } from "@/types/frigateConfig";
+import { describeSupportedDetectors } from "@/utils/plusModels";
export type FrigatePlusModel = {
id: string;
@@ -20,6 +21,8 @@ export type FrigatePlusModel = {
trainDate: string;
isBaseModel: boolean;
supportedDetectors: string[];
+ // which Hailo device a Hailo model was built for, absent on every other model
+ hailoDevice?: string;
width: number;
height: number;
};
@@ -152,7 +155,7 @@ export function ModelSourcePicker({
{describe(model)}
{t("frigatePlus.modelInfo.supportedDetectors")}:{" "}
- {model.supportedDetectors.join(", ")}
+ {describeSupportedDetectors(model)}
))
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts
index ee09fd3f96..a3ec010b37 100644
--- a/web/src/types/frigateConfig.ts
+++ b/web/src/types/frigateConfig.ts
@@ -429,6 +429,8 @@ export type DetectionModelConfig = {
baseModel: string;
isBaseModel: boolean;
supportedDetectors: string[];
+ // which Hailo device a Hailo model was built for, absent on every other model
+ hailoDevice?: string;
width: number;
height: number;
} | null;
diff --git a/web/src/utils/health.ts b/web/src/utils/health.ts
index 1f51e3d397..a1c34a126e 100644
--- a/web/src/utils/health.ts
+++ b/web/src/utils/health.ts
@@ -55,7 +55,7 @@ export function runnerNames(models: DetectionModelConfig[]): string[] {
export const PROBED_DETECTORS = new Set([
"cpu",
"edgetpu",
- "hailo8l",
+ "hailo",
"memryx",
"openvino",
"onnx",
diff --git a/web/src/utils/plusModels.ts b/web/src/utils/plusModels.ts
new file mode 100644
index 0000000000..86cdad4c39
--- /dev/null
+++ b/web/src/utils/plusModels.ts
@@ -0,0 +1,17 @@
+/** Matching Frigate+ models against the hardware that can run them. */
+
+type PlusModel = {
+ supportedDetectors: string[];
+ // which Hailo device a Hailo model was built for, absent on every other model
+ hailoDevice?: string;
+};
+
+/**
+ * The detectors to show for a model.
+ *
+ * Every Hailo model supports the one hailo detector, so the device it was
+ * built for is what the user needs to see.
+ */
+export function describeSupportedDetectors(model: PlusModel): string {
+ return model.hailoDevice ?? model.supportedDetectors.join(", ");
+}
diff --git a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx
index 9f0cd7048b..82d4b960e0 100644
--- a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx
+++ b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx
@@ -5,6 +5,7 @@ import {
} from "@/components/card/SettingsGroupCard";
import type { DetectionModelConfig } from "@/types/frigateConfig";
import { useTranslation } from "react-i18next";
+import { describeSupportedDetectors } from "@/utils/plusModels";
type FrigatePlusCurrentModelSummaryProps = {
plusModel: DetectionModelConfig["plus"];
@@ -61,7 +62,7 @@ export default function FrigatePlusCurrentModelSummary({
/>
{plusModel.supportedDetectors.join(", ")}
}
+ content={{describeSupportedDetectors(plusModel)}
}
/>
)}