Migrate Hailo detector key and support hailo device (#24327)

* Migrate Hailo detector key and support hailo device

* Fix missing check
This commit is contained in:
Nicolas Mowen
2026-09-14 08:23:36 -06:00
committed by GitHub
parent caa6edecac
commit 7821ecbb43
24 changed files with 417 additions and 61 deletions
+5 -5
View File
@@ -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
+5 -5
View File
@@ -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.
- <CommunityBadge /> [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`.
<ModelConfigDropdown detectorTitle="Hailo-8/Hailo-8L" models={objectDetectorsModels.hailo8l.models} />
<ModelConfigDropdown detectorTitle="Hailo" models={objectDetectorsModels.hailo.models} />
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.
+5 -4
View File
@@ -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.
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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/<detector>/`, where `<detector>` 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/<detector>/`, where `<detector>` 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.
+2 -2
View File
@@ -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
+2 -9
View File
@@ -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)
+8 -7
View File
@@ -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}")
+2 -2
View File
@@ -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:
@@ -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]
+46 -2
View File
@@ -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
+1 -1
View File
@@ -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())]
+87
View File
@@ -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)
+1 -1
View File
@@ -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"))
+2 -2
View File
@@ -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"), [])
+79
View File
@@ -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)
+61 -2
View File
@@ -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,
@@ -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,
}) => {
+2 -2
View File
@@ -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 ({
@@ -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({
<div>{describe(model)}</div>
<div className="text-xs text-muted-foreground">
{t("frigatePlus.modelInfo.supportedDetectors")}:{" "}
{model.supportedDetectors.join(", ")}
{describeSupportedDetectors(model)}
</div>
</SelectItem>
))
+2
View File
@@ -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;
+1 -1
View File
@@ -55,7 +55,7 @@ export function runnerNames(models: DetectionModelConfig[]): string[] {
export const PROBED_DETECTORS = new Set([
"cpu",
"edgetpu",
"hailo8l",
"hailo",
"memryx",
"openvino",
"onnx",
+17
View File
@@ -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(", ");
}
@@ -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({
/>
<SplitCardRow
label={t("frigatePlus.modelInfo.supportedDetectors")}
content={<p>{plusModel.supportedDetectors.join(", ")}</p>}
content={<p>{describeSupportedDetectors(plusModel)}</p>}
/>
</div>
)}