mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-30 18:30:34 +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:
+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,
|
||||
|
||||
Reference in New Issue
Block a user