mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 06:58:59 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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())]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"), [])
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -55,7 +55,7 @@ export function runnerNames(models: DetectionModelConfig[]): string[] {
|
||||
export const PROBED_DETECTORS = new Set([
|
||||
"cpu",
|
||||
"edgetpu",
|
||||
"hailo8l",
|
||||
"hailo",
|
||||
"memryx",
|
||||
"openvino",
|
||||
"onnx",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user