mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-27 21:18:57 +03:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29215d260 | ||
|
|
eecc43ccef | ||
|
|
7821ecbb43 | ||
|
|
caa6edecac | ||
|
|
acc740a976 | ||
|
|
3931ab74a8 | ||
|
|
5be162a22c | ||
|
|
e57eeb8288 | ||
|
|
cf03df8a98 | ||
|
|
fa36fa403b | ||
|
|
069315be33 | ||
|
|
c583e21b70 |
@@ -160,7 +160,7 @@ When reviewing code, do NOT comment on:
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
|
||||
- **Linting**: ESLint (see `web/eslint.config.js`)
|
||||
- **Formatting**: Prettier with Tailwind CSS plugin
|
||||
- **Type Safety**: TypeScript strict mode enabled
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ruff == 0.15.20
|
||||
|
||||
# types
|
||||
types-peewee == 3.17.*
|
||||
types-peewee == 4.0.*
|
||||
|
||||
@@ -3,7 +3,7 @@ click == 8.5.*
|
||||
# FastAPI
|
||||
aiohttp == 3.12.*
|
||||
starlette == 0.47.*
|
||||
starlette-context == 0.4.*
|
||||
starlette-context == 0.5.*
|
||||
fastapi[standard-no-fastapi-cloud-cli] == 0.116.*
|
||||
uvicorn == 0.46.*
|
||||
slowapi == 0.1.*
|
||||
@@ -20,7 +20,7 @@ mypy == 1.6.1
|
||||
onvif-zeep-async == 4.0.*
|
||||
paho-mqtt == 2.1.*
|
||||
pandas == 2.2.*
|
||||
peewee == 3.17.*
|
||||
peewee == 4.5.*
|
||||
peewee_migrate == 1.14.*
|
||||
psutil == 7.1.*
|
||||
pydantic == 2.10.*
|
||||
@@ -34,7 +34,7 @@ types-requests == 2.32.*
|
||||
norfair == 2.3.*
|
||||
setproctitle == 1.3.*
|
||||
ws4py == 0.5.*
|
||||
unidecode == 1.3.*
|
||||
unidecode == 1.4.*
|
||||
titlecase == 2.4.*
|
||||
# Image Manipulation
|
||||
numpy == 1.26.*
|
||||
@@ -63,12 +63,12 @@ contextlib2==0.6.*
|
||||
future==0.18.*
|
||||
netaddr==0.8.*
|
||||
netifaces==0.10.*
|
||||
prometheus-client == 0.21.*
|
||||
prometheus-client == 0.26.*
|
||||
# TFLite
|
||||
tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64'
|
||||
tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64'
|
||||
# audio transcription
|
||||
sherpa-onnx==1.12.*
|
||||
sherpa-onnx==1.13.*
|
||||
faster-whisper==1.2.*
|
||||
librosa==0.11.*
|
||||
soundfile==0.13.*
|
||||
|
||||
@@ -376,7 +376,7 @@ http {
|
||||
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
|
||||
sub_filter '"/BASE_PATH/locales/' '"$http_x_ingress_path/locales/';
|
||||
sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/';
|
||||
sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl';
|
||||
sub_filter 'return`/BASE_PATH/`' 'return window.baseUrl';
|
||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
|
||||
sub_filter_types text/css application/javascript;
|
||||
sub_filter_once off;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Nvidia ONNX Runtime GPU Support
|
||||
--extra-index-url 'https://pypi.nvidia.com'
|
||||
cython==3.0.*; platform_machine == 'x86_64'
|
||||
nvidia-cuda-cupti-cu12==12.8.90; platform_machine == 'x86_64'
|
||||
nvidia-cublas-cu12==12.8.4.1; platform_machine == 'x86_64'
|
||||
nvidia-cudnn-cu12==9.8.0.87; platform_machine == 'x86_64'
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+1632
-3641
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -18,16 +18,16 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/plugin-content-docs": "^3.7.0",
|
||||
"@docusaurus/preset-classic": "^3.7.0",
|
||||
"@docusaurus/theme-mermaid": "^3.7.0",
|
||||
"@docusaurus/core": "^3.10.2",
|
||||
"@docusaurus/plugin-content-docs": "^3.10.2",
|
||||
"@docusaurus/preset-classic": "^3.10.2",
|
||||
"@docusaurus/theme-mermaid": "^3.10.2",
|
||||
"@inkeep/docusaurus": "^2.0.16",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"clsx": "^2.1.1",
|
||||
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
||||
"docusaurus-theme-openapi-docs": "^4.5.1",
|
||||
"docusaurus-plugin-openapi-docs": "^5.2.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.2.0",
|
||||
"js-yaml": "^4.3.2",
|
||||
"marked": "^16.4.2",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
@@ -48,11 +48,11 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||
"@docusaurus/types": "^3.7.0",
|
||||
"@docusaurus/module-type-aliases": "^3.10.2",
|
||||
"@docusaurus/types": "^3.10.2",
|
||||
"@types/react": "^18.3.27"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
"node": ">=20.19"
|
||||
}
|
||||
}
|
||||
|
||||
+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)
|
||||
|
||||
|
||||
@@ -253,7 +253,7 @@ class ObjectDescriptionProcessor(PostProcessorApi):
|
||||
# Crop snapshot based on region
|
||||
# provide full image if region doesn't exist (manual events)
|
||||
height, width = img.shape[:2]
|
||||
x1_rel, y1_rel, width_rel, height_rel = event.data.get( # type: ignore[attr-defined]
|
||||
x1_rel, y1_rel, width_rel, height_rel = event.data.get(
|
||||
"region", [0, 0, 1, 1]
|
||||
)
|
||||
x1, y1 = int(x1_rel * width), int(y1_rel * height)
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import cv2
|
||||
from peewee import DoesNotExist
|
||||
@@ -528,7 +528,8 @@ class ReviewDescriptionProcessor(PostProcessorApi):
|
||||
.get()
|
||||
)
|
||||
|
||||
time_in_segment = ts - recording.start_time
|
||||
# start_time is a DateTimeField holding a unix timestamp
|
||||
time_in_segment = ts - cast(float, recording.start_time)
|
||||
return get_image_from_recording(
|
||||
self.config.ffmpeg,
|
||||
recording.path,
|
||||
|
||||
@@ -237,7 +237,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
||||
return
|
||||
|
||||
# Skip the event if not an object
|
||||
if event.data.get("type") != "object": # type: ignore[attr-defined]
|
||||
if event.data.get("type") != "object":
|
||||
return
|
||||
|
||||
thumbnail_bytes = get_event_thumbnail_bytes(event)
|
||||
|
||||
@@ -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]
|
||||
@@ -37,7 +37,7 @@ class EventCleanup(threading.Thread):
|
||||
if self.removed_camera_labels is None:
|
||||
self.removed_camera_labels = list(
|
||||
Event.select(Event.label)
|
||||
.where(Event.camera.not_in(self.camera_keys)) # type: ignore[arg-type,call-arg,misc]
|
||||
.where(Event.camera.not_in(self.camera_keys))
|
||||
.distinct()
|
||||
.execute()
|
||||
)
|
||||
@@ -89,7 +89,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.thumbnail,
|
||||
)
|
||||
.where(
|
||||
Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc]
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == event.label,
|
||||
Event.retain_indefinitely == False,
|
||||
@@ -111,7 +111,7 @@ class EventCleanup(threading.Thread):
|
||||
|
||||
# update the clips attribute for the db entry
|
||||
query = Event.select(Event.id).where(
|
||||
Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc]
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == event.label,
|
||||
Event.retain_indefinitely == False,
|
||||
@@ -218,7 +218,7 @@ class EventCleanup(threading.Thread):
|
||||
Event.camera,
|
||||
)
|
||||
.where(
|
||||
Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc]
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
@@ -249,7 +249,7 @@ class EventCleanup(threading.Thread):
|
||||
|
||||
# update the clips attribute for the db entry
|
||||
query = Event.select(Event.id).where(
|
||||
Event.camera.not_in(self.camera_keys), # type: ignore[arg-type,call-arg,misc]
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
|
||||
@@ -216,11 +216,11 @@ class ExportDebugReplaySource(DebugReplaySource):
|
||||
"""
|
||||
|
||||
def __init__(self, export: Export, duration: float) -> None:
|
||||
self._camera = cast(str, export.camera)
|
||||
self._camera = export.camera
|
||||
# Export.date is declared DateTimeField but Frigate writes raw unix
|
||||
# timestamps to the column.
|
||||
self._start_ts = float(cast(Any, export.date))
|
||||
self._video_path = cast(str, export.video_path)
|
||||
self._video_path = export.video_path
|
||||
self._duration = duration
|
||||
|
||||
@property
|
||||
|
||||
@@ -109,6 +109,8 @@ class Export(Model):
|
||||
backref="exports",
|
||||
column_name="export_case_id",
|
||||
)
|
||||
# peewee adds this accessor for the export_case column at runtime
|
||||
export_case_id: str | None
|
||||
|
||||
|
||||
class ReviewSegment(Model):
|
||||
|
||||
@@ -186,7 +186,7 @@ class NoticeRegistry:
|
||||
deleted = (
|
||||
Notice.delete()
|
||||
.where(
|
||||
Notice.kind.in_(camera_kinds), # type: ignore[call-arg, arg-type, misc]
|
||||
Notice.kind.in_(camera_kinds),
|
||||
Notice.scope == camera,
|
||||
)
|
||||
.execute()
|
||||
@@ -257,7 +257,7 @@ class NoticeRegistry:
|
||||
"""Dismissed config and stream check rows, newest first."""
|
||||
rows = (
|
||||
Notice.select()
|
||||
.where(Notice.kind.in_(list(CHECK_KINDS))) # type: ignore[call-arg, arg-type, misc]
|
||||
.where(Notice.kind.in_(list(CHECK_KINDS)))
|
||||
.order_by(Notice.dismissed_at.desc())
|
||||
)
|
||||
return [{"id": row.id, "dismissed_at": row.dismissed_at} for row in rows]
|
||||
@@ -351,7 +351,7 @@ class NoticeRegistry:
|
||||
)
|
||||
Notice.delete().where(
|
||||
Notice.kind == kind,
|
||||
Notice.id.not_in(newest), # type: ignore[call-arg, misc]
|
||||
Notice.id.not_in(newest),
|
||||
).execute()
|
||||
|
||||
def _bump_occurrences(self, kind: str, count: int, now: float) -> None:
|
||||
|
||||
+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
|
||||
|
||||
@@ -5,6 +5,7 @@ import itertools
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from multiprocessing.synchronize import Event as MpEvent
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -28,7 +29,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _filter_reviews_for_pass(
|
||||
reviews: list[Any],
|
||||
reviews: Iterable[Any],
|
||||
now: datetime.datetime,
|
||||
alerts_days: float,
|
||||
detections_days: float,
|
||||
@@ -121,14 +122,14 @@ class RecordingCleanup(threading.Thread):
|
||||
)
|
||||
|
||||
maybe_empty_dirs = set()
|
||||
thumbs_to_delete = list(map(lambda x: x[1], expired_reviews))
|
||||
for thumb_path in thumbs_to_delete:
|
||||
thumb_path = Path(thumb_path)
|
||||
thumbs_to_delete = list(map(lambda x: x.thumb_path, expired_reviews))
|
||||
for thumb in thumbs_to_delete:
|
||||
thumb_path = Path(thumb)
|
||||
thumb_path.unlink(missing_ok=True)
|
||||
maybe_empty_dirs.add(thumb_path.parent)
|
||||
|
||||
max_deletes = 100000
|
||||
deleted_reviews_list = list(map(lambda x: x[0], expired_reviews))
|
||||
deleted_reviews_list = list(map(lambda x: x.id, expired_reviews))
|
||||
for i in range(0, len(deleted_reviews_list), max_deletes):
|
||||
ReviewSegment.delete().where(
|
||||
ReviewSegment.id << deleted_reviews_list[i : i + max_deletes]
|
||||
|
||||
@@ -14,7 +14,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import pytz # type: ignore[import-untyped]
|
||||
from pathvalidate import sanitize_filename
|
||||
@@ -1055,7 +1055,10 @@ class RecordingExporter(threading.Thread):
|
||||
except DoesNotExist:
|
||||
return ""
|
||||
|
||||
diff = max(0.0, float(self.start_time) - float(preview.start_time))
|
||||
# start_time is a DateTimeField holding a unix timestamp
|
||||
diff = max(
|
||||
0.0, float(self.start_time) - float(cast(Any, preview.start_time))
|
||||
)
|
||||
ffmpeg_cmd = [
|
||||
"/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
|
||||
"-hide_banner",
|
||||
|
||||
@@ -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())]
|
||||
|
||||
|
||||
+5
-2
@@ -3,8 +3,10 @@
|
||||
import logging
|
||||
import shutil
|
||||
import threading
|
||||
from collections.abc import Iterable
|
||||
from multiprocessing.synchronize import Event as MpEvent
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from peewee import SQL, Case, fn
|
||||
|
||||
@@ -191,14 +193,15 @@ class StorageMaintainer(threading.Thread):
|
||||
|
||||
stream_usages = {
|
||||
row["stream_type"]: row["usage"] or 0
|
||||
for row in (
|
||||
for row in cast(
|
||||
Iterable[dict[str, Any]],
|
||||
Recordings.select(
|
||||
Recordings.stream_type,
|
||||
fn.SUM(Recordings.segment_size).alias("usage"),
|
||||
)
|
||||
.where(Recordings.camera == camera, Recordings.segment_size != 0)
|
||||
.group_by(Recordings.stream_type)
|
||||
.dicts()
|
||||
.dicts(),
|
||||
)
|
||||
}
|
||||
stream_bandwidths = self.camera_storage_stats.get(camera, {}).get(
|
||||
|
||||
@@ -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)
|
||||
@@ -406,12 +406,12 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
tracked_obj.obj_data["sub_label"] = (sub_label, score)
|
||||
|
||||
if event:
|
||||
event.sub_label = sub_label # type: ignore[assignment]
|
||||
event.sub_label = sub_label
|
||||
data = event.data
|
||||
if sub_label is None:
|
||||
data["sub_label_score"] = None # type: ignore[index]
|
||||
data["sub_label_score"] = None
|
||||
elif score is not None:
|
||||
data["sub_label_score"] = score # type: ignore[index]
|
||||
data["sub_label_score"] = score
|
||||
event.data = data
|
||||
event.save()
|
||||
|
||||
@@ -440,7 +440,7 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
objects_list = []
|
||||
sub_labels = set()
|
||||
events = Event.select(Event.id, Event.label, Event.sub_label).where(
|
||||
Event.id.in_(detection_ids) # type: ignore[call-arg, misc]
|
||||
Event.id.in_(detection_ids)
|
||||
)
|
||||
for det_event in events:
|
||||
if det_event.sub_label:
|
||||
@@ -506,11 +506,11 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
|
||||
if event:
|
||||
data = event.data
|
||||
data[field_name] = field_value # type: ignore[index]
|
||||
data[field_name] = field_value
|
||||
if field_value is None:
|
||||
data[f"{field_name}_score"] = None # type: ignore[index]
|
||||
data[f"{field_name}_score"] = None
|
||||
elif score is not None:
|
||||
data[f"{field_name}_score"] = score # type: ignore[index]
|
||||
data[f"{field_name}_score"] = score
|
||||
event.data = data
|
||||
event.save()
|
||||
|
||||
|
||||
+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,
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:vitest-globals/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
],
|
||||
env: { browser: true, es2021: true, "vitest-globals/env": true },
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
settings: {
|
||||
jest: {
|
||||
version: 27,
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["*.d.ts", "/src/components/ui/*"],
|
||||
plugins: ["react-hooks", "react-refresh"],
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
{
|
||||
objects: "always-multiline",
|
||||
arrays: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
},
|
||||
],
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"no-console": "error",
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Object filters settings tests -- MEDIUM tier.
|
||||
*
|
||||
* `objects.filters` is an additionalProperties map, so each label's filter is
|
||||
* an entry RJSF adds at runtime. RJSF has changed how a cleared field nested
|
||||
* inside such an entry is stored (`""` before 6.9, omitted after). These tests
|
||||
* pin what Frigate does with it: the save payload deletes only the cleared
|
||||
* key, and restoring the value leaves the section clean.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test, expect } from "../../fixtures/frigate-test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { configFactory } from "../../fixtures/mock-data/config";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_SCHEMA = JSON.parse(
|
||||
readFileSync(
|
||||
resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
|
||||
const SETTINGS_URL = "/settings?page=cameraObjects&camera=front_door";
|
||||
const UNSAVED = "You have unsaved changes";
|
||||
|
||||
// A non-default value can only come from the YAML, so deleting it is safe
|
||||
const MIN_AREA = 5000;
|
||||
|
||||
async function installRoutes(page: Page) {
|
||||
const config = configFactory({
|
||||
cameras: {
|
||||
front_door: {
|
||||
objects: {
|
||||
filters: {
|
||||
person: {
|
||||
min_area: MIN_AREA,
|
||||
max_area: 24000000,
|
||||
min_ratio: 0,
|
||||
max_ratio: 24000000,
|
||||
threshold: 0.7,
|
||||
min_score: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let lastSavedConfig: unknown = null;
|
||||
|
||||
await page.route("**/api/config/schema.json", (route) =>
|
||||
route.fulfill({ json: CONFIG_SCHEMA }),
|
||||
);
|
||||
await page.route("**/api/config", (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
return route.fulfill({ json: config });
|
||||
}
|
||||
return route.fulfill({ json: { success: true } });
|
||||
});
|
||||
await page.route("**/api/config/raw_paths", (route) =>
|
||||
route.fulfill({ json: {} }),
|
||||
);
|
||||
await page.route("**/api/config/set", async (route) => {
|
||||
lastSavedConfig = route.request().postDataJSON();
|
||||
await route.fulfill({ json: { success: true, require_restart: false } });
|
||||
});
|
||||
|
||||
return { capturedConfig: () => lastSavedConfig };
|
||||
}
|
||||
|
||||
async function openPersonMinArea(page: Page) {
|
||||
await page.getByText("Object filters", { exact: true }).click();
|
||||
await page.locator('[aria-expanded="false"]', { hasText: /^Person/ }).click();
|
||||
|
||||
const minArea = page.getByRole("textbox", { name: "Minimum object area" });
|
||||
await expect(minArea).toHaveValue(String(MIN_AREA));
|
||||
return minArea;
|
||||
}
|
||||
|
||||
test.describe("object filters additionalProperties entries @medium", () => {
|
||||
test("clearing a nested filter field deletes only that key on save", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
const capture = await installRoutes(frigateApp.page);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
const minArea = await openPersonMinArea(frigateApp.page);
|
||||
await minArea.fill("");
|
||||
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible();
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Save", exact: true })
|
||||
.click();
|
||||
|
||||
// Empty string is the backend's remove sentinel. Sibling filter fields
|
||||
// must not appear, or the save would rewrite values the user didn't touch.
|
||||
await expect
|
||||
.poll(() => capture.capturedConfig(), { timeout: 5_000 })
|
||||
.toMatchObject({
|
||||
config_data: {
|
||||
cameras: {
|
||||
front_door: { objects: { filters: { person: { min_area: "" } } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const saved = capture.capturedConfig() as {
|
||||
config_data: {
|
||||
cameras: {
|
||||
front_door: { objects: { filters: { person: object } } };
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(saved.config_data.cameras.front_door.objects.filters.person).toEqual(
|
||||
{ min_area: "" },
|
||||
);
|
||||
});
|
||||
|
||||
test("restoring a cleared nested filter field leaves the section clean", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await installRoutes(frigateApp.page);
|
||||
await frigateApp.goto(SETTINGS_URL);
|
||||
|
||||
const minArea = await openPersonMinArea(frigateApp.page);
|
||||
const save = frigateApp.page.getByRole("button", {
|
||||
name: "Save",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await minArea.fill("");
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible();
|
||||
await expect(save).toBeEnabled();
|
||||
|
||||
await minArea.fill(String(MIN_AREA));
|
||||
await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden();
|
||||
await expect(save).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -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 ({
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import path from "node:path";
|
||||
import js from "@eslint/js";
|
||||
import { defineConfig, globalIgnores, includeIgnoreFile } from "eslint/config";
|
||||
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default defineConfig([
|
||||
includeIgnoreFile(path.join(import.meta.dirname, ".gitignore")),
|
||||
globalIgnores(["src/components/ui/", "**/*.d.ts", "**/*.cjs", "**/*.mjs"]),
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
prettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: { ...globals.browser, ...globals.node },
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"no-console": "error",
|
||||
"prettier/prettier": "warn",
|
||||
},
|
||||
},
|
||||
]);
|
||||
Generated
+1993
-2216
File diff suppressed because it is too large
Load Diff
+24
-25
@@ -7,9 +7,9 @@
|
||||
"dev": "vite --host",
|
||||
"postinstall": "patch-package",
|
||||
"build": "tsc && vite build --base=/BASE_PATH/",
|
||||
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore . && npm run e2e:lint",
|
||||
"lint": "eslint . && npm run e2e:lint",
|
||||
"e2e:lint": "node e2e/scripts/lint-specs.mjs",
|
||||
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"preview": "vite preview",
|
||||
"prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"",
|
||||
"test": "vitest",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@melloware/react-logviewer": "^6.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
@@ -48,11 +48,11 @@
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@rjsf/core": "^6.4.1",
|
||||
"@rjsf/shadcn": "^6.5.2",
|
||||
"@rjsf/utils": "^6.4.1",
|
||||
"@rjsf/validator-ajv8": "^6.4.1",
|
||||
"apexcharts": "^3.52.0",
|
||||
"@rjsf/core": "^6.10.0",
|
||||
"@rjsf/shadcn": "^6.10.0",
|
||||
"@rjsf/utils": "^6.10.0",
|
||||
"@rjsf/validator-ajv8": "^6.10.0",
|
||||
"apexcharts": "^7.3.0",
|
||||
"axios": "^1.18.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -60,12 +60,11 @@
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"framer-motion": "^12.38.0",
|
||||
"framer-motion": "^13.2.0",
|
||||
"hls.js": "^1.6.15",
|
||||
"i18next": "^24.2.0",
|
||||
"i18next-http-backend": "^4.0.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"js-yaml": "^4.3.2",
|
||||
"konva": "^10.2.3",
|
||||
"lodash": "^4.18.1",
|
||||
@@ -74,7 +73,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^19.2.4",
|
||||
"react-apexcharts": "^1.4.1",
|
||||
"react-apexcharts": "^2.1.1",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -103,28 +102,27 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^6.6.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/node": "^25.9.6",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/strftime": "^0.9.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@vitejs/plugin-react-swc": "^3.8.0",
|
||||
"@vitest/coverage-v8": "^3.0.7",
|
||||
"@vitejs/plugin-react": "^6.1.1",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jest": "^28.2.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.8",
|
||||
"eslint-plugin-vitest-globals": "^1.6.1",
|
||||
"esbuild": "^0.28.2",
|
||||
"eslint": "^10.10.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.6",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"globals": "^17.12.0",
|
||||
"i18next-cli": "^1.5.11",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^24.1.1",
|
||||
@@ -136,8 +134,9 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.4.2",
|
||||
"vitest": "^3.0.7"
|
||||
"typescript-eslint": "^8.70.0",
|
||||
"vite": "^8.3.0",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
|
||||
@@ -52,7 +52,7 @@ self.addEventListener("push", function (event) {
|
||||
}
|
||||
})
|
||||
.then(show)
|
||||
: show(), // eslint-disable-line comma-dangle
|
||||
: show(),
|
||||
);
|
||||
} else {
|
||||
// pass
|
||||
@@ -77,7 +77,7 @@ self.addEventListener("notificationclick", (event) => {
|
||||
"X-CSRF-TOKEN": 1,
|
||||
},
|
||||
body: JSON.stringify({ ids: [event.notification.data.id] }),
|
||||
}), // eslint-disable-line comma-dangle
|
||||
}),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -27,7 +27,7 @@ import { LuExternalLink } from "react-icons/lu";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
type UserAuthFormProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
|
||||
const { t } = useTranslation(["components/auth", "common"]);
|
||||
|
||||
@@ -39,8 +39,6 @@ export default function AutoUpdatingCameraImage({
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [reloadInterval]);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
@@ -75,7 +73,7 @@ export default function AutoUpdatingCameraImage({
|
||||
const [isCached, setIsCached] = useState(false);
|
||||
|
||||
const cacheKey = useMemo(() => {
|
||||
let baseParam = "";
|
||||
let baseParam: string;
|
||||
|
||||
if (periodicCache && !isCached) {
|
||||
const date = new Date(key);
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function CameraImage({
|
||||
const ctx = canvasRef.current.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0, scaledWidth, scaledHeight);
|
||||
}
|
||||
onload && onload(event);
|
||||
onload?.(event);
|
||||
},
|
||||
[img, scaledHeight, scaledWidth, setHasLoaded, onload, canvasRef],
|
||||
);
|
||||
|
||||
@@ -386,6 +386,7 @@ export default function ClassificationModelEditDialog({
|
||||
"Unknown error";
|
||||
throw new Error(
|
||||
`Failed to rename ${oldName} to ${newName}: ${errorMessage}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function ClassificationModelWizardDialog({
|
||||
await axios.delete(
|
||||
`/classification/${wizardState.step1Data.modelName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Silently fail - user is already cancelling
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function Step3ChooseExamples({
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Silently fail - unselected images will remain but won't cause issues
|
||||
// since the frontend filters out images that don't match expected format
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -81,8 +81,6 @@ export function CamerasFilterButton({
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentCameras(selectedCameras);
|
||||
// only refresh when state changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedCameras]);
|
||||
|
||||
const trigger = (
|
||||
|
||||
@@ -26,14 +26,7 @@ import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
"reviewed",
|
||||
"date",
|
||||
"general",
|
||||
"motionOnly",
|
||||
] as const;
|
||||
type ReviewFilters = (typeof REVIEW_FILTERS)[number];
|
||||
type ReviewFilters = "cameras" | "reviewed" | "date" | "general" | "motionOnly";
|
||||
const DEFAULT_REVIEW_FILTERS: ReviewFilters[] = [
|
||||
"cameras",
|
||||
"reviewed",
|
||||
@@ -353,8 +346,6 @@ function GeneralFilterButton({
|
||||
showAll: showAll,
|
||||
...filter,
|
||||
});
|
||||
// only refresh when state changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedLabels, selectedZones, showAll, filter]);
|
||||
|
||||
const trigger = (
|
||||
|
||||
@@ -306,8 +306,6 @@ function GeneralFilterButton({
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLabels(selectedLabels);
|
||||
// only refresh when state changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedLabels]);
|
||||
|
||||
const trigger = (
|
||||
@@ -496,8 +494,6 @@ function SortTypeButton({
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentSortType(selectedSortType);
|
||||
// only refresh when state changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSortType]);
|
||||
|
||||
const trigger = (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { generateColors } from "@/utils/colorUtil";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import ApexCharts from "apexcharts";
|
||||
import Chart from "react-apexcharts";
|
||||
import {
|
||||
Table,
|
||||
@@ -151,10 +152,10 @@ export function CombinedStorageGraph({
|
||||
show: false,
|
||||
},
|
||||
y: {
|
||||
formatter: function (val, { seriesIndex }) {
|
||||
if (series[seriesIndex]) {
|
||||
const usage = series[seriesIndex].usage;
|
||||
return `${getUnitSize(usage)} (${val.toFixed(2)}%)`;
|
||||
formatter: function (val, opts) {
|
||||
const entry = opts ? series[opts.seriesIndex] : undefined;
|
||||
if (entry) {
|
||||
return `${getUnitSize(entry.usage)} (${val.toFixed(2)}%)`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { ApexAxisChartSeries } from "@/types/graph";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import ApexCharts from "apexcharts";
|
||||
import Chart from "react-apexcharts";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import ApexCharts from "apexcharts";
|
||||
import Chart from "react-apexcharts";
|
||||
import { getUnitSize } from "@/utils/storageUtil";
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useTheme } from "@/context/theme-provider";
|
||||
import { useDateLocale } from "@/hooks/use-date-locale";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { Threshold } from "@/types/graph";
|
||||
import { ApexAxisChartSeries, Threshold } from "@/types/graph";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import ApexCharts from "apexcharts";
|
||||
import Chart from "react-apexcharts";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -264,9 +264,9 @@ export default function InputWithTags({
|
||||
type == "max_speed"
|
||||
) {
|
||||
const newFilters = { ...filters };
|
||||
let timestamp = 0;
|
||||
let timestamp: number;
|
||||
let score = 0;
|
||||
let speed = 0;
|
||||
let speed: number;
|
||||
|
||||
switch (type) {
|
||||
case "before":
|
||||
|
||||
@@ -193,8 +193,6 @@ export default function LiveContextMenu({
|
||||
} else {
|
||||
return MdVolumeUp;
|
||||
}
|
||||
// only update when specific fields change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [volumeState, audioState]);
|
||||
|
||||
const handleVolumeIconClick = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function CreateRoleDialog({
|
||||
try {
|
||||
await onCreate(values.role, values.cameras);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Error handled in parent
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function DeleteRoleDialog({
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onDelete();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Error handled in parent
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function EditRoleCamerasDialog({
|
||||
try {
|
||||
await onSave(values.cameras);
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Error handled in parent
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -675,7 +675,7 @@ export function ExportContent({
|
||||
setSelectedOption(option);
|
||||
|
||||
const now = new Date(latestTime * 1000);
|
||||
let start = 0;
|
||||
let start: number;
|
||||
|
||||
switch (option) {
|
||||
case "1":
|
||||
|
||||
@@ -64,7 +64,7 @@ export function GenAISummaryDialog({
|
||||
const threatLevel = aiAnalysis.potential_threat_level ?? 0;
|
||||
|
||||
if (threatLevel > 0) {
|
||||
let label = "";
|
||||
let label: string;
|
||||
|
||||
switch (threatLevel) {
|
||||
case ThreatLevel.NEEDS_REVIEW:
|
||||
|
||||
@@ -599,7 +599,6 @@ export function TrackingDetails({
|
||||
playlist,
|
||||
startPosition: 0,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [event]);
|
||||
|
||||
// Determine camera aspect ratio category
|
||||
|
||||
@@ -35,7 +35,7 @@ export function GenericVideoPlayer({
|
||||
// missing media is a 404; 502 still covers a failed or
|
||||
// unreachable mapping request, which is equally unplayable
|
||||
setSourceExists(response.status !== 502 && response.status !== 404);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setSourceExists(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function JSMpegPlayer({
|
||||
try {
|
||||
videoElement.player?.destroy();
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
} catch {}
|
||||
|
||||
if (videoWrapper) {
|
||||
videoWrapper.innerHTML = "";
|
||||
|
||||
@@ -357,7 +357,7 @@ function MSEPlayer({
|
||||
if (originalHandler) {
|
||||
try {
|
||||
originalHandler(msg);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Don't reject - we got the response, just let the error bubble
|
||||
}
|
||||
}
|
||||
@@ -478,7 +478,7 @@ function MSEPlayer({
|
||||
msRef.current?.setLiveSeekableRange(end, end + 15);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
@@ -497,7 +497,7 @@ function MSEPlayer({
|
||||
} else {
|
||||
try {
|
||||
sb?.appendBuffer(data as ArrayBuffer);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +249,6 @@ function PreviewVideoPlayer({
|
||||
previewRef.current?.load();
|
||||
}, 1000);
|
||||
setChangeoverTimeout(timeout);
|
||||
|
||||
// we only want this to change when current preview changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
},
|
||||
[setCurrentHourFrame, videoSize],
|
||||
);
|
||||
|
||||
@@ -147,8 +147,6 @@ export default function VideoControls({
|
||||
} else {
|
||||
return MdVolumeUp;
|
||||
}
|
||||
// only update when specific fields change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [volume, muted]);
|
||||
|
||||
const onKeyboardShortcut = useCallback(
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function WebRtcPlayer({
|
||||
? await navigator.mediaDevices.getUserMedia(constraints)
|
||||
: await navigator.mediaDevices.getDisplayMedia(constraints);
|
||||
return stream.getTracks();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +396,7 @@ export default function ZoneEditPane({
|
||||
? `cameras.${polygon.camera}.profiles.${editingProfile}.zones.${polygon.name}`
|
||||
: `cameras.${polygon.camera}.zones.${polygon.name}`;
|
||||
|
||||
let mutatedConfig = config;
|
||||
let mutatedConfig: typeof config;
|
||||
let alertQueries = "";
|
||||
let detectionQueries = "";
|
||||
|
||||
@@ -404,9 +404,6 @@ export default function ZoneEditPane({
|
||||
|
||||
if (renamingZone) {
|
||||
// rename - delete old zone and replace with new
|
||||
let renameAlertQueries = "";
|
||||
let renameDetectionQueries = "";
|
||||
|
||||
// Only handle review queries for base config (not profiles)
|
||||
if (!editingProfile) {
|
||||
const zoneInAlerts =
|
||||
@@ -417,7 +414,7 @@ export default function ZoneEditPane({
|
||||
polygon.name,
|
||||
) ?? false;
|
||||
|
||||
({
|
||||
const {
|
||||
alertQueries: renameAlertQueries,
|
||||
detectionQueries: renameDetectionQueries,
|
||||
} = reviewQueries(
|
||||
@@ -427,7 +424,7 @@ export default function ZoneEditPane({
|
||||
polygon.camera,
|
||||
cameraConfig?.review.alerts.required_zones || [],
|
||||
cameraConfig?.review.detections.required_zones || [],
|
||||
));
|
||||
);
|
||||
|
||||
try {
|
||||
await axios.put(
|
||||
@@ -591,7 +588,6 @@ export default function ZoneEditPane({
|
||||
});
|
||||
},
|
||||
[
|
||||
config,
|
||||
updateConfig,
|
||||
polygon,
|
||||
scaledWidth,
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function Step2ProbeOrSnapshot({
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.readAsDataURL(snapshotBlob);
|
||||
});
|
||||
} catch (snapshotError) {
|
||||
} catch {
|
||||
snapshotBase64 = undefined;
|
||||
}
|
||||
}
|
||||
@@ -307,7 +307,7 @@ export default function Step2ProbeOrSnapshot({
|
||||
data.username,
|
||||
data.password,
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +236,6 @@ export function MotionReviewTimeline({
|
||||
scrollToSegment={scrollToSegment}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
getRecordingAvailability={getRecordingAvailability}
|
||||
onZoomChange={onZoomChange}
|
||||
possibleZoomLevels={possibleZoomLevels}
|
||||
currentZoomLevel={currentZoomLevel}
|
||||
|
||||
@@ -41,7 +41,6 @@ export type ReviewTimelineProps = {
|
||||
scrollToSegment: (segmentTime: number, ifNeeded?: boolean) => void;
|
||||
isZooming: boolean;
|
||||
zoomDirection: TimelineZoomDirection;
|
||||
getRecordingAvailability?: (time: number) => boolean | undefined;
|
||||
onZoomChange?: (newZoomLevel: number) => void;
|
||||
possibleZoomLevels?: ZoomLevel[];
|
||||
currentZoomLevel?: number;
|
||||
@@ -70,7 +69,6 @@ export function ReviewTimeline({
|
||||
scrollToSegment,
|
||||
isZooming,
|
||||
zoomDirection,
|
||||
getRecordingAvailability,
|
||||
onZoomChange,
|
||||
possibleZoomLevels,
|
||||
currentZoomLevel,
|
||||
@@ -353,25 +351,6 @@ export function ReviewTimeline({
|
||||
}
|
||||
}, [isDragging, onHandlebarDraggingChange]);
|
||||
|
||||
const isHandlebarInNoRecordingPeriod = useMemo(() => {
|
||||
if (!getRecordingAvailability || handlebarTime === undefined) return false;
|
||||
|
||||
// Check current segment
|
||||
const currentAvailability = getRecordingAvailability(handlebarTime);
|
||||
if (currentAvailability !== false) return false;
|
||||
|
||||
// Check if at least one adjacent segment also has no recordings
|
||||
const beforeAvailability = getRecordingAvailability(
|
||||
handlebarTime - segmentDuration,
|
||||
);
|
||||
const afterAvailability = getRecordingAvailability(
|
||||
handlebarTime + segmentDuration,
|
||||
);
|
||||
|
||||
// If current segment has no recordings AND at least one adjacent segment also has no recordings
|
||||
return beforeAvailability === false || afterAvailability === false;
|
||||
}, [getRecordingAvailability, handlebarTime, segmentDuration]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -427,12 +406,6 @@ export function ReviewTimeline({
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/* TODO: determine if we should keep this tooltip */}
|
||||
{false && isHandlebarInNoRecordingPeriod && (
|
||||
<div className="absolute left-1/2 top-full z-50 mt-2 -translate-x-1/2 rounded-md bg-destructive/80 px-4 py-1 text-center text-xs text-white shadow-lg">
|
||||
No recordings
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{showExportHandles && (
|
||||
|
||||
@@ -61,7 +61,7 @@ export function useDateLocale(): Locale {
|
||||
try {
|
||||
const loadedLocale = await localeLoader();
|
||||
setLocale(loadedLocale);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setLocale(enUS);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -153,17 +153,11 @@ export function useFullscreen<T extends HTMLElement = HTMLElement>(
|
||||
// @ts-expect-error we need to check that fullscreen exists
|
||||
if (document.exitFullscreen) return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((document as any).msExitFullscreen)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return true;
|
||||
if ((document as any).msExitFullscreen) return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((document as any).webkitExitFullscreen)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return true;
|
||||
if ((document as any).webkitExitFullscreen) return true;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((document as any).mozCancelFullScreen)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return true;
|
||||
if ((document as any).mozCancelFullScreen) return true;
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export function useOverlayState<S>(
|
||||
});
|
||||
},
|
||||
// locationRef is stable so we don't need it in deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[key, navigate, preserveSearch],
|
||||
);
|
||||
|
||||
@@ -93,7 +92,6 @@ export function usePersistedOverlayState<S extends string>(
|
||||
navigate(loc.pathname, { state: newLocationState, replace });
|
||||
},
|
||||
// locationRef is stable so we don't need it in deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[key, navigate, setPersistedValue],
|
||||
);
|
||||
|
||||
@@ -151,7 +149,6 @@ export function useUserPersistedOverlayState<S extends string>(
|
||||
navigate(loc.pathname, { state: newLocationState, replace });
|
||||
},
|
||||
// locationRef is stable so we don't need it in deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[key, navigate, setPersistedValue],
|
||||
);
|
||||
|
||||
@@ -188,7 +185,6 @@ export function useHashState<S extends string>(): [
|
||||
}
|
||||
},
|
||||
// locationRef is stable so we don't need it in deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[navigate],
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useSessionPersistence<S>(
|
||||
window.sessionStorage.setItem(key, JSON.stringify(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
});
|
||||
@@ -29,7 +29,7 @@ export function useSessionPersistence<S>(
|
||||
try {
|
||||
window.sessionStorage.setItem(key, JSON.stringify(newValue));
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (err) {}
|
||||
} catch {}
|
||||
setStoredValue(newValue);
|
||||
},
|
||||
[key],
|
||||
|
||||
@@ -81,7 +81,7 @@ function ConfigEditor() {
|
||||
"Unknown error";
|
||||
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
throw new Error(errorMessage, { cause: error });
|
||||
}
|
||||
},
|
||||
[editorRef, t],
|
||||
@@ -102,7 +102,7 @@ function ConfigEditor() {
|
||||
try {
|
||||
await onHandleSaveConfig("saveonly");
|
||||
setRestartDialogOpen(true);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// If save fails, error is already set in onHandleSaveConfig, no dialog opens
|
||||
}
|
||||
}, [onHandleSaveConfig]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const TRAIN_FILTERS = ["class", "score"] as const;
|
||||
export const TRAIN_FILTERS = ["class", "score"] as const;
|
||||
export type TrainFilters = (typeof TRAIN_FILTERS)[number];
|
||||
|
||||
export type TrainFilter = {
|
||||
|
||||
@@ -114,7 +114,7 @@ export type DeleteClipType = {
|
||||
|
||||
// filtering
|
||||
|
||||
const EXPORT_FILTERS = ["cameras"] as const;
|
||||
export const EXPORT_FILTERS = ["cameras"] as const;
|
||||
export type ExportFilters = (typeof EXPORT_FILTERS)[number];
|
||||
export const DEFAULT_EXPORT_FILTERS: ExportFilters[] = ["cameras"];
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export type FilterList = {
|
||||
|
||||
export const LAST_24_HOURS_KEY = "last24Hours";
|
||||
|
||||
const DRAWER_FEATURES = [
|
||||
export const DRAWER_FEATURES = [
|
||||
"export",
|
||||
"calendar",
|
||||
"filter",
|
||||
|
||||
@@ -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,3 +1,10 @@
|
||||
import type ApexCharts from "apexcharts";
|
||||
|
||||
// apexcharts declares this type but does not export it
|
||||
export type ApexAxisChartSeries = NonNullable<
|
||||
ApexCharts.ApexDrilldownSeries["series"]
|
||||
>;
|
||||
|
||||
export type GraphDataPoint = {
|
||||
x: Date;
|
||||
y: number;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const SEARCH_FILTERS = [
|
||||
export const SEARCH_FILTERS = [
|
||||
"cameras",
|
||||
"date",
|
||||
"time",
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function detectReolinkCamera(
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function maskUri(uri: string): string {
|
||||
urlObj.searchParams.set("password", "*".repeat(4));
|
||||
return urlObj.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return uri;
|
||||
|
||||
@@ -97,7 +97,7 @@ const formatMap: {
|
||||
const getResolvedTimeZone = () => {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const offsetMinutes = new Date().getTimezoneOffset();
|
||||
return `UTC${offsetMinutes < 0 ? "+" : "-"}${Math.abs(offsetMinutes / 60)
|
||||
.toString()
|
||||
@@ -224,7 +224,7 @@ export const formatUnixTimestampToDateTime = (
|
||||
}
|
||||
|
||||
return formattedDateTime;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return "Invalid time";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export function runnerNames(models: DetectionModelConfig[]): string[] {
|
||||
export const PROBED_DETECTORS = new Set([
|
||||
"cpu",
|
||||
"edgetpu",
|
||||
"hailo8l",
|
||||
"hailo",
|
||||
"memryx",
|
||||
"openvino",
|
||||
"onnx",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const isInIframe = (() => {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If we get a security error, we're definitely in an iframe
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function getLifecycleItemDescription(
|
||||
label,
|
||||
});
|
||||
case "attribute": {
|
||||
let title = "";
|
||||
let title: string;
|
||||
if (
|
||||
lifecycleItem.data.attribute == "face" ||
|
||||
lifecycleItem.data.attribute == "license_plate"
|
||||
|
||||
@@ -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(", ");
|
||||
}
|
||||
@@ -13,7 +13,6 @@ export function getChunkedTimeDay(timeRange: TimeRange): TimeRange[] {
|
||||
const startDay = new Date(timeRange.after * 1000);
|
||||
startDay.setUTCMinutes(0, 0, 0);
|
||||
let start = startDay.getTime() / 1000;
|
||||
let end = 0;
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
startDay.setHours(startDay.getHours() + 1);
|
||||
@@ -22,7 +21,7 @@ export function getChunkedTimeDay(timeRange: TimeRange): TimeRange[] {
|
||||
break;
|
||||
}
|
||||
|
||||
end = endOfHourOrCurrentTime(startDay.getTime() / 1000);
|
||||
const end = endOfHourOrCurrentTime(startDay.getTime() / 1000);
|
||||
data.push({
|
||||
after: start,
|
||||
before: end,
|
||||
|
||||
@@ -36,8 +36,6 @@ export const reviewQueries = (
|
||||
alertsZones: string[],
|
||||
detectionsZones: string[],
|
||||
) => {
|
||||
let alertQueries = "";
|
||||
let detectionQueries = "";
|
||||
let same_alerts = false;
|
||||
let same_detections = false;
|
||||
|
||||
@@ -50,7 +48,7 @@ export const reviewQueries = (
|
||||
alerts.delete(name);
|
||||
}
|
||||
|
||||
alertQueries = [...alerts]
|
||||
let alertQueries = [...alerts]
|
||||
.map((zone) => `&cameras.${camera}.review.alerts.required_zones=${zone}`)
|
||||
.join("");
|
||||
|
||||
@@ -63,7 +61,7 @@ export const reviewQueries = (
|
||||
detections.delete(name);
|
||||
}
|
||||
|
||||
detectionQueries = [...detections]
|
||||
let detectionQueries = [...detections]
|
||||
.map(
|
||||
(zone) => `&cameras.${camera}.review.detections.required_zones=${zone}`,
|
||||
)
|
||||
|
||||
@@ -1101,8 +1101,8 @@ function ObjectTrainGrid({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let label: string | undefined = undefined;
|
||||
let score: number | undefined = undefined;
|
||||
let label: string | undefined;
|
||||
let score: number | undefined;
|
||||
|
||||
if (model.object_config.classification_type === "attribute") {
|
||||
label = event.data[model.name] as string | undefined;
|
||||
|
||||
@@ -722,7 +722,7 @@ function DetectionReview({
|
||||
|
||||
try {
|
||||
if (node) minimapObserver.current.observe(node);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no op
|
||||
}
|
||||
},
|
||||
|
||||
@@ -668,7 +668,9 @@ export default function DraggableGridLayout({
|
||||
playInBackground={false}
|
||||
showStats={statsStates[camera.name]}
|
||||
onClick={() => {
|
||||
!isEditMode && onSelectCamera(camera.name);
|
||||
if (!isEditMode) {
|
||||
onSelectCamera(camera.name);
|
||||
}
|
||||
}}
|
||||
onError={(e) => {
|
||||
setPreferredLiveModes((prevModes) => {
|
||||
|
||||
@@ -926,7 +926,7 @@ function FrigateCameraFeatures({
|
||||
);
|
||||
setActiveToastId(toastId);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error(t("manualRecording.failedToStart"), {
|
||||
position: "top-center",
|
||||
});
|
||||
@@ -948,7 +948,7 @@ function FrigateCameraFeatures({
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
toast.error(t("manualRecording.failedToEnd"), {
|
||||
position: "top-center",
|
||||
});
|
||||
@@ -973,7 +973,7 @@ function FrigateCameraFeatures({
|
||||
xhr.setRequestHeader("X-CACHE-BYPASS", "1");
|
||||
xhr.withCredentials = true;
|
||||
xhr.send(payload);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Silently ignore errors during unload
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -243,7 +243,7 @@ export default function LiveDashboardView({
|
||||
|
||||
try {
|
||||
if (node) visibleCameraObserver.current.observe(node);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no op
|
||||
}
|
||||
},
|
||||
|
||||
@@ -672,7 +672,7 @@ export function RecordingView({
|
||||
|
||||
try {
|
||||
if (node) visiblePreviewObserver.current.observe(node);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// no op
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user