mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-21 03:09:02 +03:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd75348aa | ||
|
|
ea131e1663 | ||
|
|
e2ce0c82ff | ||
|
|
3d4dd3ac4b | ||
|
|
1ec511b66c | ||
|
|
accbab7afc | ||
|
|
cbf6d032cb | ||
|
|
933a7f1a3f | ||
|
|
4e5e8e3c59 | ||
|
|
ec3fb00494 | ||
|
|
081d6f95ef | ||
|
|
a2b46f5d84 | ||
|
|
f065cc8642 | ||
|
|
9ce80e7266 | ||
|
|
bb5056a68a | ||
|
|
b3ce4486b9 | ||
|
|
06e3d0ac5d | ||
|
|
28e3e1ec74 | ||
|
|
fa07109a85 | ||
|
|
910059281f | ||
|
|
ef44c18c07 | ||
|
|
06b059c36a | ||
|
|
26d31300e6 | ||
|
|
0013555528 |
@@ -15,12 +15,24 @@ from frigate.const import (
|
||||
)
|
||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||
from frigate.util.config import find_config_file, resolve_ffmpeg_path
|
||||
from frigate.util.services import is_restricted_go2rtc_source
|
||||
from frigate.util.services import (
|
||||
is_go2rtc_arbitrary_exec_allowed,
|
||||
is_restricted_go2rtc_source,
|
||||
)
|
||||
|
||||
sys.path.remove("/opt/frigate")
|
||||
|
||||
yaml = YAML()
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets"):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = (
|
||||
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
|
||||
)
|
||||
|
||||
config_file = find_config_file()
|
||||
|
||||
try:
|
||||
@@ -100,7 +112,7 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
|
||||
if isinstance(stream, str):
|
||||
try:
|
||||
formatted_stream = substitute_frigate_vars(stream)
|
||||
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
|
||||
if is_restricted_go2rtc_source(formatted_stream):
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
||||
@@ -119,7 +131,7 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
filtered_streams = []
|
||||
for i, stream_item in enumerate(stream):
|
||||
try:
|
||||
formatted_stream = substitute_frigate_vars(stream_item)
|
||||
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
|
||||
if is_restricted_go2rtc_source(formatted_stream):
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
||||
@@ -143,6 +155,20 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
)
|
||||
del go2rtc_config["streams"][name]
|
||||
|
||||
elif isinstance(stream, dict):
|
||||
# The map form ({"url": ...}) lets go2rtc resolve the source
|
||||
# recursively, so it is effectively a dynamic way to generate the URL
|
||||
# for a stream. That can only be backed by an exec source, so it cannot
|
||||
# be allowed unless arbitrary exec is explicitly enabled. When it is
|
||||
# enabled, leave the map untouched for go2rtc to resolve.
|
||||
if not is_go2rtc_arbitrary_exec_allowed():
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. "
|
||||
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
||||
)
|
||||
del go2rtc_config["streams"][name]
|
||||
continue
|
||||
|
||||
# add birdseye restream stream if enabled
|
||||
if config.get("birdseye", {}).get("restream", False):
|
||||
birdseye: dict[str, Any] = config.get("birdseye")
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
{
|
||||
"edgeTPU": {
|
||||
"title": "EdgeTPU",
|
||||
"models": [
|
||||
{
|
||||
"key": "mobiledet",
|
||||
"label": "Mobiledet",
|
||||
"recommended": true,
|
||||
"download": "A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.",
|
||||
"yaml": "detectors:\n coral:\n type: edgetpu\n device: usb"
|
||||
},
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": false,
|
||||
"download": "[Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize of the model) |\n| **Object detection model input height** | `320` (should match the imgsize of the model) |\n| **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` |\n| **Label map for custom object detector** | `/config/labels-coco17.txt` |",
|
||||
"yaml": "detectors:\n coral:\n type: edgetpu\n device: usb\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize of the model, typically 320\n height: 320 # <--- should match the imgsize of the model, typically 320\n path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite\n labelmap_path: /config/labels-coco17.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hailo8l": {
|
||||
"title": "Hailo-8/Hailo-8L",
|
||||
"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.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Model Input D Type** | `int` |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |\n\nThe detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override.",
|
||||
"yaml": "detectors:\n hailo:\n type: hailo8l\n device: PCIe\n\nmodel:\n width: 320\n height: 320\n input_tensor: nhwc\n input_pixel_format: rgb\n input_dtype: int\n model_type: yolo-generic\n labelmap_path: /labelmap/coco-80.txt\n\n # The detector automatically selects the default model based on your hardware:\n # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef)\n # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef)\n #\n # Optionally, you can specify a local model path to override the default.\n # If a local path is provided and the file exists, it will be used instead of downloading.\n # Example:\n # path: /config/model_cache/hailo/yolov6n.hef\n #\n # You can also override using a custom URL:\n # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef\n # just make sure to give it the write configuration based on the model"
|
||||
},
|
||||
{
|
||||
"key": "ssd",
|
||||
"label": "SSD MobileNet v1",
|
||||
"recommended": false,
|
||||
"download": "For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary. The model file is cached under `/config/model_cache/hailo`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| --------------------------------------- | ------ |\n| **Object detection model input width** | `300` |\n| **Object detection model input height** | `300` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Object Detection Model Type** | `ssd` |\n\nSpecify the local model path or URL for SSD MobileNet v1.",
|
||||
"yaml": "detectors:\n hailo:\n type: hailo8l\n device: PCIe\n\nmodel:\n width: 300\n height: 300\n input_tensor: nhwc\n input_pixel_format: rgb\n model_type: ssd\n # Specify the local model path (if available) or URL for SSD MobileNet v1.\n # Example with a local path:\n # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef\n #\n # Or override using a custom URL:\n # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"openvino": {
|
||||
"title": "OpenVINO",
|
||||
"models": [
|
||||
{
|
||||
"key": "ssd",
|
||||
"label": "SSDLite MobileNet v2",
|
||||
"recommended": true,
|
||||
"download": "An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------ |\n| **Object detection model input width** | `300` |\n| **Object detection model input height** | `300` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` |\n| **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # Or NPU\n\nmodel:\n width: 300\n height: 300\n input_tensor: nhwc\n input_pixel_format: bgr\n path: /openvino-model/ssdlite_mobilenet_v2.xml\n labelmap_path: /openvino-model/coco_91cl_bkgr.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": false,
|
||||
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # or NPU\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolo-legacy",
|
||||
"label": "YOLO (v3, v4, v7)",
|
||||
"recommended": false,
|
||||
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # or NPU\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolonas",
|
||||
"label": "YOLO-NAS",
|
||||
"recommended": false,
|
||||
"download": "You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).\n\n:::warning\n\nThe pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html\n\n:::\n\nThe input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (should match whatever was set in notebook) |\n| **Object detection model input height** | `320` (should match whatever was set in notebook) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: yolonas\n width: 320 # <--- should match whatever was set in notebook\n height: 320 # <--- should match whatever was set in notebook\n input_tensor: nchw\n input_pixel_format: bgr\n path: /config/yolo_nas_s.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolox",
|
||||
"label": "YOLOX",
|
||||
"recommended": false,
|
||||
"download": "YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ------------------------------------- | -------------------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Custom object detector model path** | path to your YOLOX ONNX model |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: yolox\n path: /config/model_cache/yolox.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "rfdetr",
|
||||
"label": "RF-DETR",
|
||||
"recommended": false,
|
||||
"download": "RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'\nFROM python:3.12 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /rfdetr\nRUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript\nARG MODEL_SIZE\nRUN python3 -c \"from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)\"\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| --------------------------------------- | --------------------------------- |\n| **Object Detection Model Type** | `rfdetr` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: rfdetr\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/rfdetr.onnx"
|
||||
},
|
||||
{
|
||||
"key": "dfine",
|
||||
"label": "D-FINE / DEIMv2",
|
||||
"recommended": false,
|
||||
"download": "#### D-FINE\n\nD-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /dfine\nRUN git clone https://github.com/Peterande/D-FINE.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx onnxruntime onnxsim onnxscript\n# Create output directory and download checkpoint\nRUN mkdir -p output\nARG MODEL_SIZE\nRUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth\n# Modify line 58 of export_onnx.py to change batch size to 1\nRUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx\nEOF\n```\n\n#### DEIMv2\n\n[DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:\n\n- **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`\n- **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`\n\nSet `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).\n\n```sh\ndocker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'\nFROM python:3.11-slim AS build\nRUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /deimv2\nRUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .\n# Install CPU-only PyTorch first to avoid pulling CUDA variant\nRUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu\nRUN uv pip install --no-cache --system -r requirements.txt\nRUN uv pip install --no-cache --system onnx safetensors huggingface_hub\nRUN mkdir -p output\nARG BACKBONE\nARG MODEL_SIZE\n# Download from Hugging Face and convert safetensors to pth\nRUN python3 -c \"\\\nfrom huggingface_hub import hf_hub_download; \\\nfrom safetensors.torch import load_file; \\\nimport torch; \\\nbackbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \\\nsize = '${MODEL_SIZE}'.upper(); \\\nst = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \\\ntorch.save({'model': st}, 'output/deimv2.pth')\"\nRUN sed -i \"s/data = torch.rand(2/data = torch.rand(1/\" tools/deployment/export_onnx.py\n# HuggingFace safetensors omits frozen constants that the model constructor initializes\nRUN sed -i \"s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/\" tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth\nFROM scratch\nARG BACKBONE\nARG MODEL_SIZE\nCOPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------------- |\n| **Object Detection Model Type** | `dfine` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n ov:\n type: openvino\n device: CPU\n\nmodel:\n model_type: dfine\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/dfine-s.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"appleSilicon": {
|
||||
"title": "Apple Silicon",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": true,
|
||||
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n apple-silicon:\n type: zmq\n endpoint: tcp://host.docker.internal:5555\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolo-legacy",
|
||||
"label": "YOLO (v3, v4, v7)",
|
||||
"recommended": false,
|
||||
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n apple-silicon:\n type: zmq\n endpoint: tcp://host.docker.internal:5555\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"onnx": {
|
||||
"title": "ONNX",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": true,
|
||||
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "rfdetr",
|
||||
"label": "RF-DETR",
|
||||
"recommended": false,
|
||||
"download": "RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'\nFROM python:3.12 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /rfdetr\nRUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript\nARG MODEL_SIZE\nRUN python3 -c \"from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)\"\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| --------------------------------------- | --------------------------------- |\n| **Object Detection Model Type** | `rfdetr` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: rfdetr\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/rfdetr.onnx"
|
||||
},
|
||||
{
|
||||
"key": "yolonas",
|
||||
"label": "YOLO-NAS",
|
||||
"recommended": false,
|
||||
"download": "You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).\n\n:::warning\n\nThe pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html\n\n:::\n\nThe input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (should match whatever was set in notebook) |\n| **Object detection model input height** | `320` (should match whatever was set in notebook) |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolonas\n width: 320 # <--- should match whatever was set in notebook\n height: 320 # <--- should match whatever was set in notebook\n input_pixel_format: bgr\n input_tensor: nchw\n path: /config/yolo_nas_s.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolox",
|
||||
"label": "YOLOX",
|
||||
"recommended": false,
|
||||
"download": "YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `416` (should match the imgsize set during model export) |\n| **Object detection model input height** | `416` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float_denorm` |\n| **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolox\n width: 416 # <--- should match the imgsize set during model export\n height: 416 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float_denorm\n path: /config/model_cache/yolox_tiny.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "dfine",
|
||||
"label": "D-FINE / DEIMv2",
|
||||
"recommended": false,
|
||||
"download": "#### Downloading D-FINE Model\n\nD-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /dfine\nRUN git clone https://github.com/Peterande/D-FINE.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx onnxruntime onnxsim onnxscript\n# Create output directory and download checkpoint\nRUN mkdir -p output\nARG MODEL_SIZE\nRUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth\n# Modify line 58 of export_onnx.py to change batch size to 1\nRUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx\nEOF\n```\n\n#### Downloading DEIMv2 Model\n\n[DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:\n\n- **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`\n- **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`\n\nSet `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).\n\n```sh\ndocker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'\nFROM python:3.11-slim AS build\nRUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /deimv2\nRUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .\n# Install CPU-only PyTorch first to avoid pulling CUDA variant\nRUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu\nRUN uv pip install --no-cache --system -r requirements.txt\nRUN uv pip install --no-cache --system onnx safetensors huggingface_hub\nRUN mkdir -p output\nARG BACKBONE\nARG MODEL_SIZE\n# Download from Hugging Face and convert safetensors to pth\nRUN python3 -c \"\\\nfrom huggingface_hub import hf_hub_download; \\\nfrom safetensors.torch import load_file; \\\nimport torch; \\\nbackbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \\\nsize = '${MODEL_SIZE}'.upper(); \\\nst = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \\\ntorch.save({'model': st}, 'output/deimv2.pth')\"\nRUN sed -i \"s/data = torch.rand(2/data = torch.rand(1/\" tools/deployment/export_onnx.py\n# HuggingFace safetensors omits frozen constants that the model constructor initializes\nRUN sed -i \"s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/\" tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth\nFROM scratch\nARG BACKBONE\nARG MODEL_SIZE\nCOPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx\nEOF\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------- |\n| **Object Detection Model Type** | `dfine` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: dfine\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/dfine_m_obj2coco.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolo-legacy",
|
||||
"label": "YOLO (v3, v4, v7)",
|
||||
"recommended": false,
|
||||
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cpu": {
|
||||
"title": "CPU",
|
||||
"models": [
|
||||
{
|
||||
"key": "ssd",
|
||||
"label": "MobileNet v2",
|
||||
"recommended": true,
|
||||
"download": "A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).\n\n| Field | Value |\n| ----------------- | ----- |\n| **Detector type** | `cpu` |\n| **Num threads** | `3` |",
|
||||
"yaml": "detectors:\n cpu1:\n type: cpu\n num_threads: 3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deepstack": {
|
||||
"title": "DeepStack / CodeProject.AI",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolo",
|
||||
"label": "YOLO",
|
||||
"recommended": true,
|
||||
"download": "This detector runs object detection over the network against a CodeProject.AI or DeepStack server, so no model is downloaded into Frigate itself. Visit the [CodeProject.AI official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to download and install the AI server on your preferred device (e.g. Raspberry Pi, Nvidia Jetson, or other compatible hardware) before configuring the detector.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection`).\n\n| Field | Value |\n| ------------- | ---------------------------------------------------------------------- |\n| **API URL** | `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection` |\n| **API Timeout** | `0.1` (seconds) |",
|
||||
"yaml": "detectors:\n deepstack:\n api_url: http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection\n type: deepstack\n api_timeout: 0.1 # seconds"
|
||||
}
|
||||
]
|
||||
},
|
||||
"memryx": {
|
||||
"title": "MemryX",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolonas",
|
||||
"label": "YOLO-NAS",
|
||||
"recommended": true,
|
||||
"download": "The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded automatically and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).\n\n**Note:** The default model for the MemryX detector is YOLO-NAS 320x320.\n\nThe input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**.\n\n- The default size of **320x320** is optimized for lower CPU usage and faster inference times.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |\n| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolonas\n width: 320 # (Can be set to 640 for higher resolution)\n height: 320 # (Can be set to 640 for higher resolution)\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolonas.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolonas.dfp (a file ending with .dfp)\n # \u2514\u2500\u2500 yolonas_post.onnx (optional; only if the model includes a cropped post-processing network)"
|
||||
},
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": false,
|
||||
"download": "The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |\n| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolo-generic\n width: 320 # (Can be set to 640 for higher resolution)\n height: 320 # (Can be set to 640 for higher resolution)\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolov9.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolov9.dfp (a file ending with .dfp)"
|
||||
},
|
||||
{
|
||||
"key": "yolox",
|
||||
"label": "YOLOX",
|
||||
"recommended": false,
|
||||
"download": "The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float_denorm` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolox\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float_denorm\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolox.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolox.dfp (a file ending with .dfp)"
|
||||
},
|
||||
{
|
||||
"key": "ssd",
|
||||
"label": "SSDLite MobileNet v2",
|
||||
"recommended": false,
|
||||
"download": "The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object Detection Model Type** | `ssd` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: ssd\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/ssdlite_mobilenet.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 ssdlite_mobilenet.dfp (a file ending with .dfp)\n # \u2514\u2500\u2500 ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tensorrt": {
|
||||
"title": "TensorRT",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolo-legacy",
|
||||
"label": "YOLO (v3, v4, v7)",
|
||||
"recommended": true,
|
||||
"download": "The model used for TensorRT must be preprocessed on the same hardware platform that it will run on, so Frigate generates the `.trt` model file on-device at startup. Processed models are stored in the `/config/model_cache` folder.\n\nBy default no models are generated. Set the `YOLO_MODELS` environment variable in Docker to one or more comma-separated model names (from the available `yolov3`/`yolov4`/`yolov7` models) and each one will be generated on startup if the corresponding `{model}.trt` file is not already present in `model_cache` (delete it to force regeneration). On Jetson devices with DLAs (Xavier or Orin), append `-dla` to a model name to generate a DLA model. If your GPU does not support FP16 operations, pass `USE_FP16=False` to disable it.\n\nAn example `docker-compose.yml` fragment that converts the `yolov7-320` and `yolov7x-640` models:\n\n```yml\nfrigate:\n environment:\n - YOLO_MODELS=yolov7-320,yolov7x-640\n - USE_FP16=false\n```",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------------------ |\n| **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |\n| **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |",
|
||||
"yaml": "detectors:\n tensorrt:\n type: tensorrt\n device: 0 #This is the default, select the first GPU\n\nmodel:\n path: /config/model_cache/tensorrt/yolov7-320.trt\n labelmap_path: /labelmap/coco-80.txt\n input_tensor: nchw\n input_pixel_format: rgb\n width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416\n height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416"
|
||||
}
|
||||
]
|
||||
},
|
||||
"synaptics": {
|
||||
"title": "Synaptics",
|
||||
"models": [
|
||||
{
|
||||
"key": "ssd",
|
||||
"label": "SSD MobileNet",
|
||||
"recommended": true,
|
||||
"download": "A synap model is provided in the container at `/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------- |\n| **Custom object detector model path** | `/synaptics/mobilenet.synap` |\n| **Object detection model input width** | `224` |\n| **Object detection model input height** | `224` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors: # required\n synap_npu: # required\n type: synaptics # required\n\nmodel: # required\n path: /synaptics/mobilenet.synap # required\n width: 224 # required\n height: 224 # required\n input_tensor: nhwc # default value (optional. If you change the model, it is required)\n labelmap_path: /labelmap/coco-80.txt # required"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rknn": {
|
||||
"title": "RKNN",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": true,
|
||||
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------- |\n| **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - frigate-fp16-yolov9-t\n # - frigate-fp16-yolov9-s\n # - frigate-fp16-yolov9-m\n # - frigate-fp16-yolov9-c\n # - frigate-fp16-yolov9-e\n # your yolo_model.rknn\n path: frigate-fp16-yolov9-t\n model_type: yolo-generic\n width: 320\n height: 320\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolonas",
|
||||
"label": "YOLO-NAS",
|
||||
"recommended": false,
|
||||
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.\n\n**Note:** The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------------------------------------------------------- |\n| **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - deci-fp16-yolonas_s\n # - deci-fp16-yolonas_m\n # - deci-fp16-yolonas_l\n # your yolonas_model.rknn\n path: deci-fp16-yolonas_s\n model_type: yolonas\n width: 320\n height: 320\n input_pixel_format: bgr\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
|
||||
},
|
||||
{
|
||||
"key": "yolox",
|
||||
"label": "YOLOx",
|
||||
"recommended": false,
|
||||
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------------------------- |\n| **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `416` |\n| **Object detection model input height** | `416` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - rock-i8-yolox_nano\n # - rock-i8-yolox_tiny\n # - rock-fp16-yolox_nano\n # - rock-fp16-yolox_tiny\n # your yolox_model.rknn\n path: rock-i8-yolox_nano\n model_type: yolox\n width: 416\n height: 416\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"axengine": {
|
||||
"title": "AXEngine",
|
||||
"models": [
|
||||
{
|
||||
"key": "yolov9",
|
||||
"label": "YOLOv9",
|
||||
"recommended": true,
|
||||
"download": "A yolov9 axmodel is provided in the container at `/axmodels` and is used by this detector type by default. The AXEngine detector downloads its default model from HuggingFace on first startup; once cached, the model works fully offline.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Custom object detector model path** | `frigate-yolov9-tiny` |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input D Type** | `int` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
|
||||
"yaml": "detectors:\n axengine:\n type: axengine\n\nmodel:\n path: frigate-yolov9-tiny\n model_type: yolo-generic\n width: 320\n height: 320\n input_dtype: int\n input_pixel_format: bgr\n labelmap_path: /labelmap/coco-80.txt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"degirumAiServer": {
|
||||
"title": "DeGirum AI Server",
|
||||
"models": [
|
||||
{
|
||||
"key": "ai-server-inference",
|
||||
"label": "AI Server Inference",
|
||||
"recommended": true,
|
||||
"download": "Launch a DeGirum AI server as a Docker container, then point the detector at it. Add this to your `docker-compose.yml`:\n\n```yaml\ndegirum_detector:\n container_name: degirum\n image: degirum/aiserver:latest\n privileged: true\n ports:\n - \"8778:8778\"\n```\n\nSet `location` to the server's service name, container name, or `host:port`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `degirum` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
|
||||
"yaml": "degirum_detector:\n type: degirum\n location: degirum\n zoo: degirum/public\n token: dg_example_token\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
"degirumLocal": {
|
||||
"title": "DeGirum Local",
|
||||
"models": [
|
||||
{
|
||||
"key": "local-inference",
|
||||
"label": "Local Inference",
|
||||
"recommended": true,
|
||||
"download": "Run hardware directly inside the Frigate container with `@local`, removing the AI server hop. The matching device runtime (e.g. the Hailo runtime) must be installed in the container; confirm it with `degirum sys-info`.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `@local` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
|
||||
"yaml": "degirum_detector:\n type: degirum\n location: @local\n zoo: degirum/public\n token: dg_example_token\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
"degirumCloud": {
|
||||
"title": "DeGirum AI Hub Cloud",
|
||||
"models": [
|
||||
{
|
||||
"key": "ai-hub-cloud-inference",
|
||||
"label": "AI Hub Cloud Inference",
|
||||
"recommended": true,
|
||||
"download": "Run inferences on DeGirum's [AI Hub](https://hub.degirum.com) cloud with `@cloud`. Sign up, create an access token, and set it as `token`. Network latency may require lowering your detection fps.",
|
||||
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `@cloud` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
|
||||
"yaml": "degirum_detector:\n type: degirum\n location: @cloud\n zoo: degirum/public\n token: dg_example_token\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ Navigate to <NavPath path="Settings > Enrichments > Face recognition" />.
|
||||
- **Detection threshold**: Face detection confidence score required before recognition runs. This field only applies to the standalone face detection model; `min_score` should be used to filter for models that have face detection built in.
|
||||
- Default: `0.7`
|
||||
- **Minimum face area**: Minimum size (in pixels) a face must be before recognition runs. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces.
|
||||
- Default: `500` pixels
|
||||
- Default: `750` pixels
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yaml">
|
||||
@@ -95,7 +95,7 @@ Navigate to <NavPath path="Settings > Enrichments > Face recognition" />.
|
||||
face_recognition:
|
||||
enabled: true
|
||||
detection_threshold: 0.7
|
||||
min_area: 500
|
||||
min_area: 750
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
@@ -671,7 +671,7 @@ lpr:
|
||||
3. Ensure your plates are being _detected_.
|
||||
|
||||
If you are using a Frigate+ or `license_plate` detecting model:
|
||||
- Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected.
|
||||
- Watch the [Debug view](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected.
|
||||
- View MQTT messages for `frigate/events` to verify detected plates.
|
||||
- You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import NavPath from "@site/src/components/NavPath";
|
||||
|
||||
Frigate has two kinds of masks: motion masks and object filter masks. Both are narrow tools for fine-tuning, **not for hiding an area from Frigate**. Masks should be used sparingly; in most cases where users reach for one, a [zone](zones.md) with `required_zones` is the right tool instead. See [Which tool do I need?](#which-tool-do-i-need) and [Common mistakes](#common-mistakes) below if you're new to Frigate's mask behavior.
|
||||
|
||||
## Motion masks
|
||||
|
||||
Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the Debug feed (Settings --> Debug) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._
|
||||
@@ -21,7 +23,16 @@ Object filter masks can be used to filter out stubborn false positives in fixed
|
||||
|
||||

|
||||
|
||||
## Creating masks
|
||||
## Which tool do I need?
|
||||
|
||||
| What you're trying to do | Recommended tool | How it works |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Don't get alerts or recordings for activity in an area (e.g., the sidewalk in front of your house) | A [zone](zones.md) combined with `review.alerts.required_zones` (and/or `review.detections.required_zones`) | Frigate keeps detecting and tracking activity in the area, but a review item is only created once the bottom-center of an object's bounding box enters a required zone. |
|
||||
| Stop a stubborn false positive at a specific fixed spot (e.g., a tree base that keeps being detected as a person) | An **object filter mask** for that object type | Any detection of that object type whose bounding-box bottom-center lands inside the mask is treated as a false positive and discarded. |
|
||||
| Ignore motion in an area that obviously isn't an object of interest (e.g., the camera timestamp, sky, flags, treetops swaying) | A **motion mask** | Motion inside the mask is ignored when deciding whether to run object detection. Objects can still be detected in a motion masked area if motion elsewhere in the frame triggers detection. |
|
||||
| Stop tracking an object type altogether on this camera (e.g., you never care about cats) | Remove the object from the camera's [`objects.track`](objects.md) list | Frigate skips this object type entirely on this camera, regardless of where it appears. |
|
||||
|
||||
## Using the mask creator
|
||||
|
||||
<ConfigTabs>
|
||||
<TabItem value="ui">
|
||||
@@ -124,3 +135,14 @@ This is what `required_zones` are for. You should define a zone (remember this i
|
||||
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
|
||||
|
||||
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
**"I added a motion mask to ignore my driveway/sidewalk."**
|
||||
A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to `review.alerts.required_zones`. Frigate will still see people on the sidewalk, but it won't create an alert until they cross into the zone.
|
||||
|
||||
**"I added an object filter mask because I don't care about cars in my yard."**
|
||||
Object filter masks are for stubborn false positives at fixed locations, not for filtering whole areas or whole object types. If you only want alerts when a car enters the driveway, use a [zone](zones.md) with `required_zones`. If you don't care about a whole object type on this camera, remove it from [`objects.track`](objects.md).
|
||||
|
||||
**"I masked everything except a thin strip on my stoop."**
|
||||
Heavy masking hurts tracking. Frigate uses motion near a tracked object's previous bounding box to decide where to look in the next frame; with most of the frame masked, an object walking from an unmasked area into a masked one effectively disappears and gets picked up as a "new" object when it reappears. For example: someone walks down your sidewalk, stops under a tree (masked area) to tie their shoe, then continues. Frigate sees that as two separate people and can create two separate review items. Because Frigate needs several consecutive frames above the confidence threshold to commit to a detection, each re-appearance can also delay or miss alerts. Use `required_zones` for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact.
|
||||
|
||||
@@ -59,6 +59,8 @@ Metrics are available at `/api/metrics` by default. No additional Frigate config
|
||||
- `frigate_storage_used_bytes{storage=""}` - Storage used bytes
|
||||
- `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info
|
||||
|
||||
These gauges report the operating system's figures for the whole filesystem (the same numbers as `df`), not Frigate's own recording footprint. For how this differs from the recordings usage shown in the UI, see [Understanding storage usage](/configuration/record#understanding-storage-usage).
|
||||
|
||||
### Service Metrics
|
||||
|
||||
- `frigate_service_uptime_seconds` - Uptime in seconds
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -199,10 +199,6 @@ Because recording segments are written in 10 second chunks, pre-capture timing d
|
||||
|
||||
Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk** — they do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there.
|
||||
|
||||
## Will Frigate delete old recordings if my storage runs out?
|
||||
|
||||
If there is less than an hour left of storage, the oldest hour of recordings will be deleted and a message will be printed in the Frigate logs. This emergency cleanup deletes the oldest recordings first regardless of retention settings to reclaim space as quickly as possible.
|
||||
|
||||
## Configuring Recording Retention
|
||||
|
||||
Frigate supports both continuous and tracked object based recordings with separate retention modes and retention periods.
|
||||
@@ -355,3 +351,63 @@ Setting `verbose: true` writes a detailed report of every orphaned file and data
|
||||
This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended.
|
||||
|
||||
:::
|
||||
|
||||
## Understanding storage usage
|
||||
|
||||
The storage usage Frigate reports will not exactly match what the operating system reports with `df` or `du`. This is expected, not a bug. The sections below explain how Frigate derives its storage figures and why they differ from the disk's own accounting.
|
||||
|
||||
### How Frigate measures recording usage
|
||||
|
||||
The **Recordings** value on the Storage Metrics page (<NavPath path="System > Storage" />) — and the per-camera **Camera Storage** breakdown — is the sum of the recording segment sizes Frigate has written, taken from Frigate's database. It is **not** computed by a scan of the disk. Frigate tracks usage this way by design: repeatedly walking the entire drive to total its size would keep hard drives spun up and add unnecessary I/O.
|
||||
|
||||
The disk **total** shown beside it, and the free-space figure Frigate uses to decide when to delete recordings, instead come from the operating system's report for the whole filesystem mounted at `/media/frigate`. As a result, the **Unused** value on the page is _total disk capacity minus Frigate's recordings_ — not the drive's real free space, which will be lower whenever anything else is stored on the disk.
|
||||
|
||||
### What counts toward usage — and why it won't match `df`
|
||||
|
||||
Only **recording segments** (`/media/frigate/recordings`) are included in the recordings storage total. Plenty of other things consume real disk space but are **not** part of that number:
|
||||
|
||||
- **Snapshots and thumbnails** (`/media/frigate/clips`) — see [Snapshots](/configuration/snapshots). These are retained independently of recordings.
|
||||
- **Preview videos** and **review thumbnails** (also under `/media/frigate/clips`).
|
||||
- **Exports** (`/media/frigate/exports`) — exports are never removed by retention.
|
||||
- **The database, downloaded detection models, and face / license plate training images** (stored under `/config`).
|
||||
- **Debug images from enrichments** (`/media/frigate/clips`) — when enabled, License Plate Recognition's `debug_save_plates` and GenAI's `debug_save_thumbnails` save plate crops and request images for troubleshooting.
|
||||
|
||||
These files are the usual explanation for an "other" or seemingly unaccounted bucket of space — it is real, it is Frigate's, and it simply isn't part of the _recordings_ total. They are also why comparing the **Recordings** figure to `df -h` always shows a gap: `df` additionally counts any non-Frigate data on the disk, filesystem overhead and reserved blocks (ext4 reserves ~5% for root by default, so a disk can read "full" before recordings approach the total), and recently deleted recordings whose space has not yet been reclaimed.
|
||||
|
||||
:::tip
|
||||
|
||||
The Storage page is not intended to be a system-wide disk monitor — it shows how much space _Frigate's recordings_ use. To see true disk usage, use `df -h` (free space) and `du -sh` (per-directory usage) on the host.
|
||||
|
||||
:::
|
||||
|
||||
### Free space and the `/media/frigate` mount
|
||||
|
||||
Frigate reports the capacity and free space of whatever filesystem is actually mounted at `/media/frigate` **inside the container**. If an external drive or network share isn't truly mounted there — a missing `/etc/fstab` entry, a share that was offline when the container started, or a host that doesn't pass the path through — the container falls back to the host's OS disk, and Frigate will correctly report that smaller disk instead of the drive you intended.
|
||||
|
||||
If the reported capacity doesn't match your drive, the mount is the place to look, not Frigate. Verify what is actually mounted from inside the container:
|
||||
|
||||
```bash
|
||||
docker exec -it frigate df -h /media/frigate
|
||||
docker exec -it frigate mount | grep media
|
||||
```
|
||||
|
||||
See the [storage mount layout](/frigate/installation#storage) for how the volumes are expected to be configured.
|
||||
|
||||
### The `/tmp/cache` area is separate
|
||||
|
||||
Recording segments are first written to `/tmp/cache` — a small, in-memory (`tmpfs`) area — before being checked and moved to `/media/frigate/recordings`. Because it is separate and small, `/tmp/cache` can fill up and produce `No space left on device` errors even when the recordings disk has plenty of room — they are different storage areas. See [Recordings troubleshooting](/troubleshooting/recordings) for diagnosing cache and slow-storage issues.
|
||||
|
||||
### When the metrics don't match what's on disk
|
||||
|
||||
Because usage is tracked in the database, deleting recording files directly on disk — or files left behind after an upgrade — will not update the reported usage, and can even push it above 100%. Frigate is unaware of files it didn't record and won't count or remove them automatically. Use [Syncing Media Files With Disk](#syncing-media-files-with-disk) to reconcile the database with what is actually on disk.
|
||||
|
||||
## Will Frigate delete old recordings if my storage runs out?
|
||||
|
||||
Yes. Frigate continuously checks the **free space of the disk** holding `/media/frigate/recordings`. This is different from adding up the size of every recording: free space is a single number the operating system already tracks, so Frigate can ask for it instantly without reading through your files or spinning up the disk — which is exactly why it relies on this check rather than scanning the drive. When less than roughly one hour of recording space remains — estimated from the current recording bitrate, **not** a fixed percentage — Frigate deletes the oldest recordings to reclaim space and logs a message. This emergency cleanup removes the oldest recordings first **regardless of retention settings**.
|
||||
|
||||
Two consequences follow from this being based on whole-disk free space:
|
||||
|
||||
- Because the check uses the disk's real free space, **anything** filling the drive — including non-Frigate files — can trigger deletion of your oldest recordings.
|
||||
- Cleanup can run while a meaningful percentage of the disk is still free (for example, with high bitrates or many cameras), because the threshold is "less than ~1 hour of recording headroom," not "X% full."
|
||||
|
||||
Frequent emergency cleanups usually mean your configured retention exceeds what the disk can hold. Reduce your retention days so the normal retention cleanup keeps up and the emergency path rarely triggers.
|
||||
|
||||
@@ -68,26 +68,26 @@ Frigate supports multiple different detectors that work on different types of ha
|
||||
**AMD**
|
||||
|
||||
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection
|
||||
- [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models)
|
||||
- [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector)
|
||||
- Runs best on discrete AMD GPUs
|
||||
|
||||
**Apple Silicon**
|
||||
|
||||
- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection
|
||||
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models)
|
||||
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector)
|
||||
- Runs well with any size models including large
|
||||
- Runs via ZMQ proxy which adds some latency, only recommended for local connection
|
||||
|
||||
**Intel**
|
||||
|
||||
- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection.
|
||||
- [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models)
|
||||
- [Supports majority of model architectures](../../configuration/object_detectors#openvino-detector)
|
||||
- Runs best with tiny, small, or medium models
|
||||
|
||||
**Nvidia**
|
||||
|
||||
- [Nvidia GPU](#nvidia-gpus): Nvidia GPUs can provide efficient object detection.
|
||||
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models)
|
||||
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx)
|
||||
- Runs well with any size models including large
|
||||
|
||||
- <CommunityBadge /> [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6.
|
||||
|
||||
@@ -42,6 +42,8 @@ Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011
|
||||
|
||||
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
|
||||
|
||||
Once running, see [Understanding storage usage](/configuration/record#understanding-storage-usage) for how Frigate measures and reports disk usage — and why its numbers won't exactly match `df` or `du`.
|
||||
|
||||
#### SSDs (Solid State Drives)
|
||||
|
||||
SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs.
|
||||
|
||||
@@ -124,3 +124,19 @@ cameras:
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
### Why does Frigate keep creating new events for my parked car?
|
||||
|
||||
Stationary tracking is designed to _prevent_ this — a parked car should stay one tracked object and not generate new events. If you're getting repeated events for the same car, it's likely that Frigate is losing the tracked object and re-detecting it as a new one.
|
||||
|
||||
Open one of the events in Explore → **Tracking Details**. If the detection scores are low (< 70% or so), the model isn't confident the parked car is a car. This is common with the free [COCO-trained](https://cocodataset.org/#explore) object detection models on steep/top-down angles, partially occluded cars, foliage, or low-light footage. When detections fall below `min_score` for too many frames the tracker loses the object, and the next confident frame creates a brand new one.
|
||||
|
||||
What helps:
|
||||
|
||||
- **Improve the view** — even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking.
|
||||
- **Use a more accurate model** — switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
|
||||
- **Don't set `detect -> stationary -> max_frames` for `car`** — it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md).
|
||||
- **Restrict alerts to the areas you care about** with `required_zones` — see [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves.
|
||||
- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc.
|
||||
|
||||
See [Object Filters](../configuration/object_filters.md) for more on tuning `min_score` and `threshold` — note that raising them too high will make this exact problem worse.
|
||||
|
||||
@@ -9,6 +9,12 @@ import NavPath from "@site/src/components/NavPath";
|
||||
|
||||
This page describes how to _use_ the Explore view. For how the underlying features are _configured_, see [Semantic Search](/configuration/semantic_search) and [Generative AI descriptions](/configuration/genai/genai_objects).
|
||||
|
||||
:::tip
|
||||
|
||||
If you just want to quickly see what happened on your cameras, it's recommended to use [Review](/usage/review) rather than Explore. Review groups overlapping and adjacent activity on a camera into **review items** and sorts them into Alerts, Detections, and Motion, so you can scan and play back footage in a few clicks instead of sifting through individual objects. Reach for Explore when you need to find a _specific_ tracked object after the fact — by label, time, zone, or description.
|
||||
|
||||
:::
|
||||
|
||||
## Browsing tracked objects
|
||||
|
||||
The default view shows your most recent tracked objects grouped into rows by label — _Person_, _Car_, _Dog_, and so on — each row labeled with the object type and a count. The arrow at the end of a row opens the full, filterable grid for that label.
|
||||
|
||||
@@ -29,7 +29,7 @@ If you see **"No recordings found for this time"**, the most common causes are:
|
||||
|
||||
A toggle (a drawer on mobile) switches the side panel between three modes:
|
||||
|
||||
- **Timeline** — a scrubbable vertical timeline of the selected camera, annotated with a motion line, review-item markers, and gaps where no recording exists.
|
||||
- **Timeline** — a scrubbable vertical timeline of the selected camera. Horizontal lines down the center represent motion, with longer lines indicating more motion at that moment. Review items are marked as shaded areas (**red** for alerts, **orange** for detections), and sections with no colored background are times when no recording exists.
|
||||
- **Events** — a scrollable list of the camera's review items for the time range; clicking one seeks the player to it.
|
||||
- **Detail** — the [tracking details inspector](#the-detail-view) for the objects in view.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ This page describes how to _use_ the Live view. For how to _configure_ live stre
|
||||
|
||||
The default **All Cameras** dashboard shows every camera, with a filmstrip of recent **alerts** scrolling across the top. Clicking an alert opens it in [Review](/usage/review); each card also has a check button to mark it reviewed without leaving the dashboard. Only **alerts** appear in the filmstrip — to suppress a label or zone from showing there, configure it as a detection instead (see [Alerts and Detections](/configuration/review#alerts-and-detections)).
|
||||
|
||||
By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this per camera or per group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies).
|
||||
By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this for each camera when using a camera group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies).
|
||||
|
||||
On mobile, a toggle in the header switches between a **grid** layout and a single-column **list** layout. On desktop a **fullscreen** button is available in the lower-right corner.
|
||||
|
||||
@@ -24,7 +24,7 @@ The icon rail (top-left on desktop, a horizontal strip on mobile) switches betwe
|
||||
- The **home** icon is the **All Cameras** dashboard, which shows every camera enabled for the dashboard.
|
||||
- Each **camera group** you create appears as its own icon. Selecting a group shows only that group's cameras.
|
||||
|
||||
Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and streaming preferences.
|
||||
Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and camera streaming preferences.
|
||||
|
||||
You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open it directly at `http://<frigate_host>:5000/#birdseye`. Clicking a camera inside the Birdseye view jumps to that camera's live feed.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
||||
"docusaurus-theme-openapi-docs": "^4.5.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"marked": "^16.4.2",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { useState } from "react";
|
||||
import CodeBlock from "@theme/CodeBlock";
|
||||
import ConfigTabs from "@site/src/components/ConfigTabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import { marked } from "marked";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
marked.setOptions({ gfm: true });
|
||||
|
||||
/**
|
||||
* @typedef {Object} Model
|
||||
* @property {string} key
|
||||
* @property {string} label
|
||||
* @property {boolean} recommended
|
||||
* @property {string} download Markdown for the "download the model" step.
|
||||
* @property {string} ui Markdown for the Frigate UI configuration step.
|
||||
* @property {string} yaml Raw YAML for the configuration step.
|
||||
*/
|
||||
|
||||
// Render a markdown string to React nodes. Fenced code blocks become Docusaurus
|
||||
// CodeBlock components (so they get syntax highlighting and a copy button);
|
||||
// everything else is marked-parsed to HTML.
|
||||
function renderBlocks(md, keyPrefix) {
|
||||
if (!md.trim()) return [];
|
||||
const tokens = marked.lexer(md);
|
||||
const nodes = [];
|
||||
let buffer = [];
|
||||
let idx = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (buffer.length) {
|
||||
buffer.links = tokens.links;
|
||||
nodes.push(
|
||||
<div
|
||||
key={`${keyPrefix}-h${idx++}`}
|
||||
dangerouslySetInnerHTML={{ __html: marked.parser(buffer) }}
|
||||
/>,
|
||||
);
|
||||
buffer = [];
|
||||
}
|
||||
};
|
||||
|
||||
tokens.forEach((token) => {
|
||||
if (token.type === "code") {
|
||||
flush();
|
||||
const language = (token.lang || "text").split(/\s+/)[0];
|
||||
nodes.push(
|
||||
<CodeBlock key={`${keyPrefix}-c${idx++}`} language={language}>
|
||||
{token.text}
|
||||
</CodeBlock>,
|
||||
);
|
||||
} else {
|
||||
buffer.push(token);
|
||||
}
|
||||
});
|
||||
flush();
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// marked does not understand Docusaurus admonitions (:::warning ... :::), so
|
||||
// render those blocks ourselves and render everything around them normally.
|
||||
function renderMarkdown(md) {
|
||||
if (!md) return null;
|
||||
const admonition = /:::(\w+)[ \t]*([^\n]*)\n([\s\S]*?)\n:::/g;
|
||||
const nodes = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
let k = 0;
|
||||
while ((match = admonition.exec(md)) !== null) {
|
||||
nodes.push(...renderBlocks(md.slice(lastIndex, match.index), `seg${k}`));
|
||||
const [, type, title, body] = match;
|
||||
const heading = (title || type).trim();
|
||||
nodes.push(
|
||||
<div
|
||||
key={`adm${k}`}
|
||||
className={`${styles.admonition} ${styles[`admonition_${type}`] || ""}`}
|
||||
>
|
||||
<div className={styles.admonitionTitle}>{heading}</div>
|
||||
{renderBlocks(body, `adm${k}`)}
|
||||
</div>,
|
||||
);
|
||||
lastIndex = admonition.lastIndex;
|
||||
k++;
|
||||
}
|
||||
nodes.push(...renderBlocks(md.slice(lastIndex), `seg${k}`));
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function Markdown({ children }) {
|
||||
return <div className={styles.markdown}>{renderMarkdown(children)}</div>;
|
||||
}
|
||||
|
||||
function RecommendedBadge() {
|
||||
return <span className={styles.recommendedBadge}>Recommended</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ models: Model[] }} props
|
||||
*/
|
||||
export default function ModelConfigDropdown({ models }) {
|
||||
const [selectedModelIndex, setSelectedModelIndex] = useState(0);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const selectedModel = models[selectedModelIndex];
|
||||
const hasChoices = models.length > 1;
|
||||
|
||||
const handleModelSelect = (index) => {
|
||||
setSelectedModelIndex(index);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.step}>
|
||||
<h4 className={styles.stepTitle}>Step 1 — Choose a model</h4>
|
||||
<div
|
||||
className={`${styles.dropdown} ${isOpen ? styles.open : ""} ${
|
||||
hasChoices ? "" : styles.static
|
||||
}`}
|
||||
onClick={hasChoices ? () => setIsOpen(!isOpen) : undefined}
|
||||
>
|
||||
<div className={styles.dropdownContent}>
|
||||
<span className={styles.modelName}>
|
||||
{selectedModel.label}
|
||||
{selectedModel.recommended && <RecommendedBadge />}
|
||||
</span>
|
||||
{hasChoices && (
|
||||
<span className={styles.arrow}>{isOpen ? "▲" : "▼"}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isOpen && hasChoices && (
|
||||
<div className={styles.menu}>
|
||||
{models.map((model, index) => (
|
||||
<div
|
||||
key={model.key}
|
||||
className={`${styles.menuItem} ${
|
||||
index === selectedModelIndex ? styles.menuItemActive : ""
|
||||
}`}
|
||||
onClick={() => handleModelSelect(index)}
|
||||
>
|
||||
{model.label}
|
||||
{model.recommended && <RecommendedBadge />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<h4 className={styles.stepTitle}>Step 2 — Download the model</h4>
|
||||
<Markdown>{selectedModel.download}</Markdown>
|
||||
</div>
|
||||
|
||||
<div className={styles.step}>
|
||||
<h4 className={styles.stepTitle}>Step 3 — Configure the detector</h4>
|
||||
<ConfigTabs>
|
||||
<TabItem value="ui">
|
||||
<Markdown>{selectedModel.ui}</Markdown>
|
||||
</TabItem>
|
||||
<TabItem value="yaml">
|
||||
<CodeBlock language="yaml">{selectedModel.yaml}</CodeBlock>
|
||||
</TabItem>
|
||||
</ConfigTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/* ===================================================================
|
||||
ModelConfigDropdown — styles
|
||||
=================================================================== */
|
||||
|
||||
.wrapper {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* --- Dropdown button --- */
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
text-align: left;
|
||||
border: 1px solid var(--ifm-color-emphasis-400);
|
||||
border-radius: 8px;
|
||||
background: var(--ifm-background-color);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
[data-theme="light"] .dropdown {
|
||||
border: 1px solid #d0d7de;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dropdown {
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
.dropdown:hover {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dropdown:hover {
|
||||
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
|
||||
}
|
||||
|
||||
.dropdown.open {
|
||||
border-color: var(--ifm-color-primary);
|
||||
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dropdown.open {
|
||||
border-color: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
/* Single-model detectors render the label without a clickable menu. */
|
||||
.dropdown.static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dropdown.static:hover {
|
||||
border-color: var(--ifm-color-emphasis-400);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-theme="light"] .dropdown.static:hover {
|
||||
border-color: #d0d7de;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .dropdown.static:hover {
|
||||
border-color: var(--ifm-color-emphasis-300);
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.8rem 1rem;
|
||||
}
|
||||
|
||||
/* --- Model menu --- */
|
||||
|
||||
.menu {
|
||||
margin-top: 0.25rem;
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--ifm-color-emphasis-400);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--ifm-background-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .menu {
|
||||
border: 1px solid #d0d7de;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .menu {
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
color: var(--ifm-font-color-base);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.menuItem:not(:last-child) {
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.menuItem:hover {
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.menuItemActive {
|
||||
font-weight: var(--ifm-font-weight-semibold);
|
||||
background: var(--ifm-color-primary-lightest);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .menuItem:hover {
|
||||
background: #2b3139;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .menuItemActive {
|
||||
background: #2b3139;
|
||||
}
|
||||
|
||||
.modelName {
|
||||
font-weight: var(--ifm-font-weight-semibold);
|
||||
color: var(--ifm-font-color-base);
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recommendedBadge {
|
||||
display: inline-block;
|
||||
background: var(--ifm-color-success);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 0.7rem;
|
||||
color: var(--ifm-font-color-secondary);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.dropdown.open .arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* --- Panel --- */
|
||||
|
||||
.panel {
|
||||
margin-top: 0.5rem;
|
||||
border: 1px solid var(--ifm-color-emphasis-400);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--ifm-background-color);
|
||||
}
|
||||
|
||||
[data-theme="light"] .panel {
|
||||
border: 1px solid #d0d7de;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .panel {
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
background: #21262d;
|
||||
}
|
||||
|
||||
/* --- Steps --- */
|
||||
|
||||
.step {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.step:not(:last-child) {
|
||||
border-bottom: 1px solid var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.stepTitle {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--ifm-font-color-base);
|
||||
}
|
||||
|
||||
/* Rendered markdown (download + Frigate UI instructions). */
|
||||
|
||||
.markdown {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.markdown table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin: 0.75rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Docusaurus-style admonitions rendered from markdown. */
|
||||
|
||||
.admonition {
|
||||
margin: 0.75rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 4px solid var(--ifm-color-info);
|
||||
border-radius: 4px;
|
||||
background: var(--ifm-color-info-contrast-background);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.admonition > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.admonitionTitle {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--ifm-color-info);
|
||||
}
|
||||
|
||||
.admonition_warning {
|
||||
border-left-color: var(--ifm-color-warning);
|
||||
background: var(--ifm-color-warning-contrast-background);
|
||||
}
|
||||
|
||||
.admonition_warning .admonitionTitle {
|
||||
color: var(--ifm-color-warning-dark);
|
||||
}
|
||||
|
||||
.admonition_danger {
|
||||
border-left-color: var(--ifm-color-danger);
|
||||
background: var(--ifm-color-danger-contrast-background);
|
||||
}
|
||||
|
||||
.admonition_danger .admonitionTitle {
|
||||
color: var(--ifm-color-danger-dark);
|
||||
}
|
||||
|
||||
.admonition_tip {
|
||||
border-left-color: var(--ifm-color-success);
|
||||
background: var(--ifm-color-success-contrast-background);
|
||||
}
|
||||
|
||||
.admonition_tip .admonitionTitle {
|
||||
color: var(--ifm-color-success-dark);
|
||||
}
|
||||
Vendored
+14
@@ -7393,6 +7393,13 @@ components:
|
||||
required:
|
||||
- value
|
||||
title: CameraSetBody
|
||||
ChaptersEnum:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- recording_segments
|
||||
- review_items
|
||||
title: ChaptersEnum
|
||||
ChatCompletionRequest:
|
||||
properties:
|
||||
messages:
|
||||
@@ -8119,6 +8126,13 @@ components:
|
||||
- type: 'null'
|
||||
title: Export case ID
|
||||
description: ID of the export case to assign this export to
|
||||
chapters:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/ChaptersEnum'
|
||||
- type: 'null'
|
||||
title: Chapter mode
|
||||
description: Optional chapter metadata to embed in the export. When
|
||||
omitted, the camera's configured export chapter mode is used.
|
||||
type: object
|
||||
title: ExportRecordingsBody
|
||||
ExportRecordingsCustomBody:
|
||||
|
||||
@@ -147,6 +147,19 @@ def go2rtc_camera_stream(request: Request, stream_name: str):
|
||||
)
|
||||
def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
|
||||
"""Add or update a go2rtc stream configuration."""
|
||||
if src and is_restricted_go2rtc_source(src):
|
||||
logger.warning(
|
||||
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
||||
stream_name,
|
||||
)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": "Restricted stream source type",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
params = {"name": stream_name}
|
||||
if src:
|
||||
|
||||
@@ -3,7 +3,10 @@ from typing import Optional, Union
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from frigate.record.export import PlaybackSourceEnum
|
||||
from frigate.record.export import (
|
||||
ChaptersEnum,
|
||||
PlaybackSourceEnum,
|
||||
)
|
||||
|
||||
|
||||
class ExportRecordingsBody(BaseModel):
|
||||
@@ -18,6 +21,14 @@ class ExportRecordingsBody(BaseModel):
|
||||
max_length=30,
|
||||
description="ID of the export case to assign this export to",
|
||||
)
|
||||
chapters: Optional[ChaptersEnum] = Field(
|
||||
default=None,
|
||||
title="Chapter mode",
|
||||
description=(
|
||||
"Optional chapter metadata to embed in the export. When omitted, "
|
||||
"the camera's configured export chapter mode is used."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ExportRecordingsCustomBody(BaseModel):
|
||||
|
||||
@@ -68,6 +68,7 @@ from frigate.jobs.export import (
|
||||
from frigate.models import Export, ExportCase, Previews, Recordings
|
||||
from frigate.record.export import (
|
||||
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
|
||||
ChaptersEnum,
|
||||
PlaybackSourceEnum,
|
||||
validate_ffmpeg_args,
|
||||
)
|
||||
@@ -128,6 +129,15 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons
|
||||
def _sanitize_existing_image(
|
||||
image_path: Optional[str],
|
||||
) -> tuple[Optional[str], Optional[JSONResponse]]:
|
||||
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
|
||||
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
|
||||
# escapes the directory once resolved. A valid snapshot path never uses "..".
|
||||
if image_path and ".." in image_path:
|
||||
return None, JSONResponse(
|
||||
content={"success": False, "message": "Invalid image path"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
existing_image = sanitize_filepath(image_path) if image_path else None
|
||||
|
||||
if existing_image and not existing_image.startswith(CLIPS_DIR):
|
||||
@@ -254,6 +264,7 @@ def _build_export_job(
|
||||
ffmpeg_input_args: Optional[str] = None,
|
||||
ffmpeg_output_args: Optional[str] = None,
|
||||
cpu_fallback: bool = False,
|
||||
chapters: Optional[ChaptersEnum] = None,
|
||||
) -> ExportJob:
|
||||
return ExportJob(
|
||||
id=_generate_export_id(camera_name),
|
||||
@@ -267,6 +278,7 @@ def _build_export_job(
|
||||
ffmpeg_input_args=ffmpeg_input_args,
|
||||
ffmpeg_output_args=ffmpeg_output_args,
|
||||
cpu_fallback=cpu_fallback,
|
||||
chapters=chapters,
|
||||
)
|
||||
|
||||
|
||||
@@ -725,6 +737,9 @@ def export_recordings_batch(
|
||||
sanitized_images[index],
|
||||
PlaybackSourceEnum.recordings,
|
||||
export_case_id,
|
||||
chapters=request.app.frigate_config.cameras[
|
||||
item.camera
|
||||
].record.export.chapters,
|
||||
)
|
||||
try:
|
||||
start_export_job(request.app.frigate_config, export_job)
|
||||
@@ -803,6 +818,14 @@ def export_recording(
|
||||
|
||||
export_case_id = body.export_case_id
|
||||
|
||||
# a chapters value in the request body overrides the camera's export config
|
||||
camera_config = request.app.frigate_config.cameras[camera_name]
|
||||
chapters = (
|
||||
body.chapters
|
||||
if body.chapters is not None
|
||||
else camera_config.record.export.chapters
|
||||
)
|
||||
|
||||
# Attaching to an existing case requires admin. Single-export for
|
||||
# cameras the user can access is otherwise non-admin; we only gate
|
||||
# the case-attachment side effect.
|
||||
@@ -839,6 +862,7 @@ def export_recording(
|
||||
existing_image,
|
||||
playback_source,
|
||||
export_case_id,
|
||||
chapters=chapters,
|
||||
)
|
||||
try:
|
||||
start_export_job(request.app.frigate_config, export_job)
|
||||
|
||||
+32
-13
@@ -60,6 +60,19 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=[Tags.media])
|
||||
|
||||
|
||||
def _resolve_cache_age(max_cache_age: int) -> int:
|
||||
"""Return max_cache_age as an int.
|
||||
|
||||
When a media handler is invoked directly by another handler instead of
|
||||
through its route, FastAPI doesn't resolve the Query() default and
|
||||
max_cache_age arrives as the Query object; fall back to its int default.
|
||||
"""
|
||||
if isinstance(max_cache_age, int):
|
||||
return max_cache_age
|
||||
|
||||
return max_cache_age.default
|
||||
|
||||
|
||||
@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)])
|
||||
async def mjpeg_feed(
|
||||
request: Request,
|
||||
@@ -413,7 +426,9 @@ async def submit_recording_snapshot_to_plus(
|
||||
)
|
||||
|
||||
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
|
||||
request.app.frigate_config.plus_api.upload_image(nd, camera_name)
|
||||
await asyncio.to_thread(
|
||||
request.app.frigate_config.plus_api.upload_image, nd, camera_name
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
@@ -936,7 +951,7 @@ async def event_thumbnail(
|
||||
thumbnail_bytes,
|
||||
media_type=extension.get_mime_type(),
|
||||
headers={
|
||||
"Cache-Control": f"private, max-age={max_cache_age}"
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}"
|
||||
if event_complete
|
||||
else "no-store",
|
||||
},
|
||||
@@ -1270,14 +1285,14 @@ async def event_preview(request: Request, event_id: str):
|
||||
end_ts = start_ts + (
|
||||
min(event.end_time - event.start_time, 20) if event.end_time else 20
|
||||
)
|
||||
return preview_gif(request, event.camera, start_ts, end_ts)
|
||||
return await preview_gif(request, event.camera, start_ts, end_ts)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif",
|
||||
dependencies=[Depends(require_camera_access)],
|
||||
)
|
||||
def preview_gif(
|
||||
async def preview_gif(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
start_ts: float,
|
||||
@@ -1340,7 +1355,8 @@ def preview_gif(
|
||||
"-",
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1419,7 +1435,8 @@ def preview_gif(
|
||||
"-",
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
input=str.encode("\n".join(selected_previews)),
|
||||
capture_output=True,
|
||||
@@ -1438,7 +1455,7 @@ def preview_gif(
|
||||
gif_bytes,
|
||||
media_type="image/gif",
|
||||
headers={
|
||||
"Cache-Control": f"private, max-age={max_cache_age}",
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
|
||||
"Content-Type": "image/gif",
|
||||
},
|
||||
)
|
||||
@@ -1448,7 +1465,7 @@ def preview_gif(
|
||||
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4",
|
||||
dependencies=[Depends(require_camera_access)],
|
||||
)
|
||||
def preview_mp4(
|
||||
async def preview_mp4(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
start_ts: float,
|
||||
@@ -1528,7 +1545,8 @@ def preview_mp4(
|
||||
path,
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1604,7 +1622,8 @@ def preview_mp4(
|
||||
path,
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
input=str.encode("\n".join(selected_previews)),
|
||||
capture_output=True,
|
||||
@@ -1619,7 +1638,7 @@ def preview_mp4(
|
||||
|
||||
headers = {
|
||||
"Content-Description": "File Transfer",
|
||||
"Cache-Control": f"private, max-age={max_cache_age}",
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
|
||||
"Content-Type": "video/mp4",
|
||||
"Content-Length": str(os.path.getsize(path)),
|
||||
# nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
||||
@@ -1657,9 +1676,9 @@ async def review_preview(
|
||||
)
|
||||
|
||||
if format == "gif":
|
||||
return preview_gif(request, review.camera, start_ts, end_ts)
|
||||
return await preview_gif(request, review.camera, start_ts, end_ts)
|
||||
else:
|
||||
return preview_mp4(request, review.camera, start_ts, end_ts)
|
||||
return await preview_mp4(request, review.camera, start_ts, end_ts)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
+39
-7
@@ -72,11 +72,16 @@ _WS_VIEWER_TOPICS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
|
||||
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
|
||||
|
||||
|
||||
def _check_ws_authorization(
|
||||
topic: str,
|
||||
role_header: str | None,
|
||||
separator: str,
|
||||
roles_config: dict[str, list[str]] | None = None,
|
||||
camera_names: set[str] | None = None,
|
||||
) -> bool:
|
||||
"""Check if a WebSocket message is authorized.
|
||||
|
||||
@@ -84,6 +89,10 @@ def _check_ws_authorization(
|
||||
topic: The message topic.
|
||||
role_header: The HTTP_REMOTE_ROLE header value, or None.
|
||||
separator: The role separator character from proxy config.
|
||||
roles_config: The auth.roles mapping (role -> allowed cameras), used to
|
||||
authorize camera-scoped commands for non-admin users.
|
||||
camera_names: All configured camera names, used to resolve a role's
|
||||
allowed cameras.
|
||||
|
||||
Returns:
|
||||
True if authorized, False if blocked.
|
||||
@@ -93,16 +102,33 @@ def _check_ws_authorization(
|
||||
return False
|
||||
|
||||
# No role header: default to viewer (fail-closed)
|
||||
if role_header is None:
|
||||
return topic in _WS_VIEWER_TOPICS
|
||||
roles = [r.strip() for r in role_header.split(separator)] if role_header else []
|
||||
|
||||
# Check if any role is admin
|
||||
roles = [r.strip() for r in role_header.split(separator)]
|
||||
# Admin can send anything
|
||||
if "admin" in roles:
|
||||
return True
|
||||
|
||||
# Non-admin: only viewer topics allowed
|
||||
return topic in _WS_VIEWER_TOPICS
|
||||
# Read-only topics any authenticated user can send
|
||||
if topic in _WS_VIEWER_TOPICS:
|
||||
return True
|
||||
|
||||
# Camera-scoped command like "<camera>/ptz": allow when the user's role(s)
|
||||
# grant access to that camera.
|
||||
parts = topic.split("/")
|
||||
if (
|
||||
roles_config is not None
|
||||
and len(parts) == 2
|
||||
and parts[1] in _WS_CAMERA_COMMAND_TOPICS
|
||||
):
|
||||
allowed: set[str] = set()
|
||||
# No role header maps to the default viewer role (e.g. proxy-only setups)
|
||||
for role in roles or ["viewer"]:
|
||||
allowed.update(
|
||||
User.get_allowed_cameras(role, roles_config, camera_names or set())
|
||||
)
|
||||
return parts[0] in allowed
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ---- Outbound filtering ---------------------------------------------------
|
||||
@@ -449,6 +475,8 @@ class WebSocketClient(Communicator):
|
||||
class _WebSocketHandler(WebSocket):
|
||||
receiver = self._dispatcher
|
||||
role_separator = self.config.proxy.separator or ","
|
||||
roles_config = self.config.auth.roles
|
||||
camera_names = set(self.config.cameras.keys())
|
||||
|
||||
def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined]
|
||||
try:
|
||||
@@ -470,7 +498,11 @@ class WebSocketClient(Communicator):
|
||||
self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None
|
||||
)
|
||||
if self.environ is not None and not _check_ws_authorization(
|
||||
topic, role_header, self.role_separator
|
||||
topic,
|
||||
role_header,
|
||||
self.role_separator,
|
||||
self.roles_config,
|
||||
self.camera_names,
|
||||
):
|
||||
logger.warning(
|
||||
"Blocked unauthorized WebSocket message: topic=%s, role=%s",
|
||||
|
||||
@@ -9,6 +9,7 @@ from frigate.review.types import SeverityEnum
|
||||
from ..base import FrigateBaseModel
|
||||
|
||||
__all__ = [
|
||||
"ChaptersEnum",
|
||||
"RecordConfig",
|
||||
"RecordExportConfig",
|
||||
"RecordPreviewConfig",
|
||||
@@ -86,6 +87,12 @@ class RecordPreviewConfig(FrigateBaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ChaptersEnum(str, Enum):
|
||||
none = "none"
|
||||
recording_segments = "recording_segments"
|
||||
review_items = "review_items"
|
||||
|
||||
|
||||
class RecordExportConfig(FrigateBaseModel):
|
||||
hwaccel_args: Union[str, list[str]] = Field(
|
||||
default="auto",
|
||||
@@ -98,6 +105,10 @@ class RecordExportConfig(FrigateBaseModel):
|
||||
title="Maximum concurrent exports",
|
||||
description="Maximum number of export jobs to process at the same time.",
|
||||
)
|
||||
chapters: ChaptersEnum = Field(
|
||||
default=ChaptersEnum.review_items,
|
||||
title="Chapter metadata to embed in exported recordings",
|
||||
)
|
||||
|
||||
|
||||
class RecordConfig(FrigateBaseModel):
|
||||
|
||||
@@ -13,6 +13,7 @@ from peewee import DoesNotExist
|
||||
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.config.camera.record import ChaptersEnum
|
||||
from frigate.const import UPDATE_JOB_STATE
|
||||
from frigate.jobs.job import Job
|
||||
from frigate.models import Export
|
||||
@@ -55,6 +56,7 @@ class ExportJob(Job):
|
||||
ffmpeg_input_args: Optional[str] = None
|
||||
ffmpeg_output_args: Optional[str] = None
|
||||
cpu_fallback: bool = False
|
||||
chapters: Optional[ChaptersEnum] = None
|
||||
current_step: str = "queued"
|
||||
progress_percent: float = 0.0
|
||||
|
||||
@@ -343,6 +345,7 @@ class ExportJobManager:
|
||||
job.ffmpeg_input_args,
|
||||
job.ffmpeg_output_args,
|
||||
job.cpu_fallback,
|
||||
job.chapters,
|
||||
on_progress=self._make_progress_callback(job),
|
||||
)
|
||||
|
||||
|
||||
+113
-5
@@ -17,6 +17,7 @@ import pytz # type: ignore[import-untyped]
|
||||
from peewee import DoesNotExist
|
||||
|
||||
from frigate.config import FfmpegConfig, FrigateConfig
|
||||
from frigate.config.camera.record import ChaptersEnum
|
||||
from frigate.const import (
|
||||
CACHE_DIR,
|
||||
CLIPS_DIR,
|
||||
@@ -217,6 +218,7 @@ class RecordingExporter(threading.Thread):
|
||||
ffmpeg_input_args: Optional[str] = None,
|
||||
ffmpeg_output_args: Optional[str] = None,
|
||||
cpu_fallback: bool = False,
|
||||
chapters: Optional[ChaptersEnum] = None,
|
||||
on_progress: Optional[Callable[[str, float], None]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@@ -232,6 +234,7 @@ class RecordingExporter(threading.Thread):
|
||||
self.ffmpeg_input_args = ffmpeg_input_args
|
||||
self.ffmpeg_output_args = ffmpeg_output_args
|
||||
self.cpu_fallback = cpu_fallback
|
||||
self.chapters = chapters
|
||||
self.on_progress = on_progress
|
||||
|
||||
# ensure export thumb dir
|
||||
@@ -509,6 +512,74 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
return meta_path
|
||||
|
||||
def _build_recording_segment_chapter_metadata_file(
|
||||
self, recordings: list
|
||||
) -> Optional[str]:
|
||||
"""Write an FFmpeg metadata file with one chapter per recording segment.
|
||||
|
||||
Each chapter's title is the segment's wallclock start time in
|
||||
strict ISO 8601 form so a viewer can map any point in the
|
||||
export's playback timeline back to real-world time without
|
||||
OCR-ing a burnt-in timestamp. Chapter offsets are computed in
|
||||
*output time*: the VOD endpoint concatenates recording clips
|
||||
back-to-back, so wall-clock gaps between recordings collapse in
|
||||
the produced video. Returns ``None`` when there are no
|
||||
recordings or every segment is empty after clipping.
|
||||
"""
|
||||
if not recordings:
|
||||
return None
|
||||
|
||||
tz_name = self.config.ui.timezone
|
||||
tz: Optional[datetime.tzinfo] = None
|
||||
if tz_name:
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
tz = None
|
||||
if tz is None:
|
||||
tz = datetime.timezone.utc
|
||||
|
||||
chapter_blocks: list[str] = []
|
||||
output_offset_ms = 0
|
||||
for rec in recordings:
|
||||
clipped_start = max(float(rec.start_time), float(self.start_time))
|
||||
clipped_end = min(float(rec.end_time), float(self.end_time))
|
||||
if clipped_end <= clipped_start:
|
||||
continue
|
||||
|
||||
duration_ms = int(round((clipped_end - clipped_start) * 1000))
|
||||
if duration_ms <= 0:
|
||||
continue
|
||||
|
||||
title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat(
|
||||
timespec="seconds"
|
||||
)
|
||||
chapter_blocks.append(
|
||||
"[CHAPTER]\n"
|
||||
"TIMEBASE=1/1000\n"
|
||||
f"START={output_offset_ms}\n"
|
||||
f"END={output_offset_ms + duration_ms}\n"
|
||||
f"title={title}"
|
||||
)
|
||||
output_offset_ms += duration_ms
|
||||
|
||||
if not chapter_blocks:
|
||||
return None
|
||||
|
||||
meta_path = self._chapter_metadata_path()
|
||||
try:
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
f.write(";FFMETADATA1\n")
|
||||
f.write("\n".join(chapter_blocks))
|
||||
f.write("\n")
|
||||
except OSError:
|
||||
logger.exception(
|
||||
"Failed to write chapter metadata file for export %s", self.export_id
|
||||
)
|
||||
return None
|
||||
|
||||
return meta_path
|
||||
|
||||
def save_thumbnail(self, id: str) -> str:
|
||||
thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp")
|
||||
|
||||
@@ -672,7 +743,18 @@ class RecordingExporter(threading.Thread):
|
||||
)
|
||||
).split(" ")
|
||||
else:
|
||||
chapters_path = self._build_chapter_metadata_file(recordings)
|
||||
# Realtime/stream-copy export. Embed chapter metadata according to
|
||||
# the camera's configured chapter mode: per-recording-segment
|
||||
# timestamps or per-review-item titles.
|
||||
if self.chapters == ChaptersEnum.recording_segments:
|
||||
chapters_path = self._build_recording_segment_chapter_metadata_file(
|
||||
recordings
|
||||
)
|
||||
elif self.chapters == ChaptersEnum.review_items:
|
||||
chapters_path = self._build_chapter_metadata_file(recordings)
|
||||
else:
|
||||
chapters_path = None
|
||||
|
||||
chapter_args = (
|
||||
f" -i {chapters_path} -map 0 -dn -map_metadata 1"
|
||||
if chapters_path
|
||||
@@ -684,7 +766,19 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
# add metadata
|
||||
title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
|
||||
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
|
||||
creation_time = datetime.datetime.fromtimestamp(
|
||||
self.start_time, tz=datetime.timezone.utc
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
ffmpeg_cmd.extend(
|
||||
[
|
||||
"-metadata",
|
||||
f"title={title}",
|
||||
"-metadata",
|
||||
f"creation_time={creation_time}",
|
||||
"-metadata",
|
||||
f"comment=Camera: {self.camera}",
|
||||
]
|
||||
)
|
||||
|
||||
ffmpeg_cmd.append(video_path)
|
||||
|
||||
@@ -770,18 +864,32 @@ class RecordingExporter(threading.Thread):
|
||||
self.config.ffmpeg.ffmpeg_path,
|
||||
hwaccel_args,
|
||||
f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
|
||||
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
|
||||
f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
|
||||
EncodeTypeEnum.timelapse,
|
||||
)
|
||||
).split(" ")
|
||||
else:
|
||||
ffmpeg_cmd = (
|
||||
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
|
||||
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
|
||||
).split(" ")
|
||||
|
||||
# add metadata
|
||||
title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
|
||||
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
|
||||
creation_time = datetime.datetime.fromtimestamp(
|
||||
self.start_time, tz=datetime.timezone.utc
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
ffmpeg_cmd.extend(
|
||||
[
|
||||
"-metadata",
|
||||
f"title={title}",
|
||||
"-metadata",
|
||||
f"creation_time={creation_time}",
|
||||
"-metadata",
|
||||
f"comment=Camera: {self.camera}",
|
||||
]
|
||||
)
|
||||
|
||||
ffmpeg_cmd.append(video_path)
|
||||
|
||||
return ffmpeg_cmd, playlist_lines
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ from frigate.util.services import get_video_properties
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STALE_RECORDINGS_INFO_TTL = MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2
|
||||
|
||||
|
||||
class SegmentInfo:
|
||||
def __init__(
|
||||
@@ -301,6 +303,8 @@ class RecordingMaintainer(threading.Thread):
|
||||
RecordingsDataTypeEnum.saved.value,
|
||||
)
|
||||
|
||||
self._expire_stale_recordings_info(grouped_recordings)
|
||||
|
||||
recordings_to_insert: list[Optional[dict[str, Any]]] = await asyncio.gather(
|
||||
*tasks
|
||||
)
|
||||
@@ -311,6 +315,21 @@ class RecordingMaintainer(threading.Thread):
|
||||
[r for r in recordings_to_insert if r is not None],
|
||||
)
|
||||
|
||||
def _expire_stale_recordings_info(
|
||||
self, grouped_recordings: defaultdict[str, list[dict[str, Any]]]
|
||||
) -> None:
|
||||
expire_before = datetime.datetime.now().timestamp() - STALE_RECORDINGS_INFO_TTL
|
||||
for recordings_info in (
|
||||
self.object_recordings_info,
|
||||
self.audio_recordings_info,
|
||||
):
|
||||
for camera in list(recordings_info.keys()):
|
||||
if camera in grouped_recordings:
|
||||
continue
|
||||
info = recordings_info[camera]
|
||||
while info and info[0][0] < expire_before:
|
||||
info.pop(0)
|
||||
|
||||
def drop_segment(self, cache_path: str) -> None:
|
||||
Path(cache_path).unlink(missing_ok=True)
|
||||
self.end_time_cache.pop(cache_path, None)
|
||||
@@ -631,6 +650,8 @@ class RecordingMaintainer(threading.Thread):
|
||||
"copy",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"-metadata",
|
||||
f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}",
|
||||
file_path,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
|
||||
@@ -115,6 +115,46 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNone(result)
|
||||
maintainer.drop_segment.assert_called_once_with(cache_path)
|
||||
|
||||
async def test_expire_stale_recordings_info_drops_only_absent_cameras(self):
|
||||
config = MagicMock(spec=FrigateConfig)
|
||||
config.cameras = {}
|
||||
stop_event = MagicMock()
|
||||
maintainer = RecordingMaintainer(config, stop_event)
|
||||
|
||||
now = datetime.datetime.now().timestamp()
|
||||
ancient = now - 86400
|
||||
recent = now - 1
|
||||
|
||||
maintainer.object_recordings_info["present_cam"] = [(ancient, [], [], [])]
|
||||
maintainer.audio_recordings_info["present_cam"] = [(ancient, 0, [])]
|
||||
|
||||
maintainer.object_recordings_info["absent_cam"] = [
|
||||
(ancient, [], [], []),
|
||||
(recent, [], [], []),
|
||||
]
|
||||
maintainer.audio_recordings_info["absent_cam"] = [
|
||||
(ancient, 0, []),
|
||||
(recent, 0, []),
|
||||
]
|
||||
|
||||
grouped_recordings = {"present_cam": [{"start_time": ancient}]}
|
||||
|
||||
maintainer._expire_stale_recordings_info(grouped_recordings)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.object_recordings_info["present_cam"], [(ancient, [], [], [])]
|
||||
)
|
||||
self.assertEqual(
|
||||
maintainer.audio_recordings_info["present_cam"], [(ancient, 0, [])]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
maintainer.object_recordings_info["absent_cam"], [(recent, [], [], [])]
|
||||
)
|
||||
self.assertEqual(
|
||||
maintainer.audio_recordings_info["absent_cam"], [(recent, 0, [])]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import random
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from frigate.track.tracked_object import TrackedObjectAttribute
|
||||
from frigate.util.object import average_boxes
|
||||
|
||||
|
||||
class TestBoxStatistics(unittest.TestCase):
|
||||
def test_average_boxes_matches_numpy(self) -> None:
|
||||
rng = random.Random(0)
|
||||
for _ in range(5000):
|
||||
boxes = [
|
||||
[rng.randint(0, 4000) for _ in range(4)]
|
||||
for _ in range(rng.randint(1, 10))
|
||||
]
|
||||
expected = [float(np.mean([b[i] for b in boxes])) for i in range(4)]
|
||||
self.assertEqual(average_boxes(boxes), expected)
|
||||
|
||||
|
||||
class TestAttribute(unittest.TestCase):
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Tests for stationary object classification thresholds."""
|
||||
|
||||
import unittest
|
||||
|
||||
from frigate.track.stationary_classifier import (
|
||||
DEFAULT_OBJECT_THRESHOLDS,
|
||||
DYNAMIC_OBJECT_THRESHOLDS,
|
||||
NON_STATIONARY_OBJECT_THRESHOLDS,
|
||||
STATIONARY_OBJECT_THRESHOLDS,
|
||||
StationaryThresholds,
|
||||
get_stationary_threshold,
|
||||
)
|
||||
|
||||
|
||||
class TestStationaryThresholds(unittest.TestCase):
|
||||
def test_known_labels_return_expected_singletons(self) -> None:
|
||||
self.assertIs(get_stationary_threshold("package"), STATIONARY_OBJECT_THRESHOLDS)
|
||||
self.assertIs(get_stationary_threshold("car"), DYNAMIC_OBJECT_THRESHOLDS)
|
||||
self.assertIs(
|
||||
get_stationary_threshold("license_plate"),
|
||||
NON_STATIONARY_OBJECT_THRESHOLDS,
|
||||
)
|
||||
|
||||
def test_unknown_label_returns_shared_default(self) -> None:
|
||||
# an unknown label must reuse the shared default instance, not allocate
|
||||
# a fresh one on every call (this runs per object per frame)
|
||||
first = get_stationary_threshold("person")
|
||||
second = get_stationary_threshold("dog")
|
||||
self.assertIs(first, DEFAULT_OBJECT_THRESHOLDS)
|
||||
self.assertIs(second, DEFAULT_OBJECT_THRESHOLDS)
|
||||
|
||||
def test_default_matches_a_fresh_instance(self) -> None:
|
||||
# the shared default must be value-equivalent to the previous
|
||||
# per-call StationaryThresholds()
|
||||
self.assertEqual(DEFAULT_OBJECT_THRESHOLDS, StationaryThresholds())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -82,6 +82,24 @@ class TestRegion(unittest.TestCase):
|
||||
|
||||
assert len(cluster_candidates) == 2
|
||||
|
||||
def test_cluster_candidates_partition_boxes(self):
|
||||
# every box index must appear in exactly one cluster (no box used twice,
|
||||
# none dropped) - the invariant the used-box tracking enforces
|
||||
boxes = [
|
||||
(100, 100, 200, 200),
|
||||
(202, 150, 252, 200),
|
||||
(210, 160, 260, 210),
|
||||
(900, 900, 950, 950),
|
||||
(905, 905, 955, 955),
|
||||
]
|
||||
|
||||
cluster_candidates = get_cluster_candidates(
|
||||
self.frame_shape, self.min_region_size, boxes
|
||||
)
|
||||
|
||||
assigned = [idx for cluster in cluster_candidates for idx in cluster]
|
||||
self.assertEqual(sorted(assigned), list(range(len(boxes))))
|
||||
|
||||
def test_transliterate_to_latin(self):
|
||||
self.assertEqual(transliterate_to_latin("frégate"), "fregate")
|
||||
self.assertEqual(transliterate_to_latin("utilité"), "utilite")
|
||||
|
||||
@@ -11,6 +11,16 @@ class TestCheckWsAuthorization(unittest.TestCase):
|
||||
|
||||
DEFAULT_SEPARATOR = ","
|
||||
|
||||
# admin/viewer are reserved and always map to all cameras (empty list);
|
||||
# custom roles map to a specific set of cameras.
|
||||
ROLES_CONFIG = {
|
||||
"admin": [],
|
||||
"viewer": [],
|
||||
"yard": ["front_door", "backyard"],
|
||||
"garage_only": ["garage"],
|
||||
}
|
||||
CAMERA_NAMES = {"front_door", "backyard", "garage"}
|
||||
|
||||
# --- IPC topic blocking (unconditional, regardless of role) ---
|
||||
|
||||
def test_ipc_topic_blocked_for_admin(self):
|
||||
@@ -161,6 +171,124 @@ class TestCheckWsAuthorization(unittest.TestCase):
|
||||
_check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR)
|
||||
)
|
||||
|
||||
# --- Camera-scoped PTZ access (non-admin with camera access) ---
|
||||
|
||||
def test_viewer_can_ptz_camera_with_access(self):
|
||||
# viewer maps to all cameras, so PTZ is allowed
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_custom_role_can_ptz_assigned_camera(self):
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_custom_role_blocked_from_ptz_unassigned_camera(self):
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_multiple_roles_union_camera_access_for_ptz(self):
|
||||
# "yard" covers front_door/backyard, "garage_only" covers garage
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"yard,garage_only",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_unknown_role_blocked_from_ptz(self):
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"nonexistent",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_no_role_header_treated_as_viewer_for_ptz(self):
|
||||
# proxy-only / auth-disabled setups default to the viewer role
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
None,
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_camera_access_does_not_grant_set_commands(self):
|
||||
# camera access enables PTZ only, not config-changing "set" commands
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/detect/set",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_ptz_autotracker_stays_admin_only(self):
|
||||
# ptz_autotracker is a config toggle, not a live-view action
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz_autotracker/set",
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_admin_can_ptz_any_camera_with_config(self):
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"admin",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_ipc_topic_still_blocked_with_camera_access(self):
|
||||
# IPC topics are blocked unconditionally, even with camera access
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
UPDATE_CAMERA_ACTIVITY,
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -641,9 +641,11 @@ class NorfairTracker(ObjectTracker):
|
||||
self.deregister(self.track_id_map[e_id], e_id)
|
||||
|
||||
# update list of object boxes that don't have a tracked object yet
|
||||
tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()]
|
||||
tracked_object_boxes = {
|
||||
tuple(obj["box"]) for obj in self.tracked_objects.values()
|
||||
}
|
||||
self.untracked_object_boxes = [
|
||||
o[2] for o in detections if o[2] not in tracked_object_boxes
|
||||
o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes
|
||||
]
|
||||
|
||||
def print_objects_as_table(self, tracked_objects: Sequence) -> None:
|
||||
|
||||
@@ -63,6 +63,9 @@ NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds(
|
||||
max_stationary_history=4,
|
||||
)
|
||||
|
||||
# Default thresholds for any other object label
|
||||
DEFAULT_OBJECT_THRESHOLDS = StationaryThresholds()
|
||||
|
||||
|
||||
def get_stationary_threshold(label: str) -> StationaryThresholds:
|
||||
"""Get the stationary thresholds for a given object label."""
|
||||
@@ -76,7 +79,7 @@ def get_stationary_threshold(label: str) -> StationaryThresholds:
|
||||
if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects:
|
||||
return NON_STATIONARY_OBJECT_THRESHOLDS
|
||||
|
||||
return StationaryThresholds()
|
||||
return DEFAULT_OBJECT_THRESHOLDS
|
||||
|
||||
|
||||
class StationaryMotionClassifier:
|
||||
|
||||
+15
-19
@@ -339,18 +339,13 @@ def reduce_boxes(boxes, iou_threshold=0.0):
|
||||
|
||||
def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]:
|
||||
"""Return a box that is the average of a list of boxes."""
|
||||
x_mins = []
|
||||
y_mins = []
|
||||
x_max = []
|
||||
y_max = []
|
||||
|
||||
for box in boxes:
|
||||
x_mins.append(box[0])
|
||||
y_mins.append(box[1])
|
||||
x_max.append(box[2])
|
||||
y_max.append(box[3])
|
||||
|
||||
return [np.mean(x_mins), np.mean(y_mins), np.mean(x_max), np.mean(y_max)]
|
||||
n = len(boxes)
|
||||
return [
|
||||
sum(box[0] for box in boxes) / n,
|
||||
sum(box[1] for box in boxes) / n,
|
||||
sum(box[2] for box in boxes) / n,
|
||||
sum(box[3] for box in boxes) / n,
|
||||
]
|
||||
|
||||
|
||||
def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]:
|
||||
@@ -401,13 +396,13 @@ def get_cluster_candidates(frame_shape, min_region, boxes):
|
||||
# determined by the max_region size minus half the box + 20%
|
||||
# TODO: see if we can do this with numpy
|
||||
cluster_candidates = []
|
||||
used_boxes = []
|
||||
used_boxes = set()
|
||||
# loop over each box
|
||||
for current_index, b in enumerate(boxes):
|
||||
if current_index in used_boxes:
|
||||
continue
|
||||
cluster = [current_index]
|
||||
used_boxes.append(current_index)
|
||||
used_boxes.add(current_index)
|
||||
cluster_boundary = get_cluster_boundary(b, min_region)
|
||||
# find all other boxes that fit inside the boundary
|
||||
for compare_index, compare_box in enumerate(boxes):
|
||||
@@ -436,7 +431,7 @@ def get_cluster_candidates(frame_shape, min_region, boxes):
|
||||
|
||||
if should_cluster:
|
||||
cluster.append(compare_index)
|
||||
used_boxes.append(compare_index)
|
||||
used_boxes.add(compare_index)
|
||||
cluster_candidates.append(cluster)
|
||||
|
||||
# return the unique clusters only
|
||||
@@ -558,6 +553,7 @@ def reduce_detections(
|
||||
current_detection = sorted_by_area[current_detection_idx]
|
||||
current_label = current_detection[0]
|
||||
current_box = current_detection[2]
|
||||
current_area = area(current_box)
|
||||
overlap = 0
|
||||
for to_check_idx in range(
|
||||
min(current_detection_idx + 1, len(sorted_by_area)),
|
||||
@@ -568,14 +564,14 @@ def reduce_detections(
|
||||
# if area of current detection / area of check < 5% they should not be compared
|
||||
# this covers cases where a large car parked in a driveway doesn't block detections
|
||||
# of cars in the street behind it
|
||||
if area(current_box) / area(to_check) < 0.05:
|
||||
if current_area / area(to_check) < 0.05:
|
||||
continue
|
||||
|
||||
intersect_box = intersection(current_box, to_check)
|
||||
# if % of smaller detection is inside of another detection, consolidate
|
||||
if intersect_box is not None and area(intersect_box) / area(
|
||||
current_box
|
||||
) > LABEL_CONSOLIDATION_MAP.get(
|
||||
if intersect_box is not None and area(
|
||||
intersect_box
|
||||
) / current_area > LABEL_CONSOLIDATION_MAP.get(
|
||||
current_label, LABEL_CONSOLIDATION_DEFAULT
|
||||
):
|
||||
overlap = 1
|
||||
|
||||
@@ -790,7 +790,7 @@ def get_hailo_temps() -> dict[str, float]:
|
||||
return temps
|
||||
|
||||
|
||||
def _go2rtc_arbitrary_exec_allowed() -> bool:
|
||||
def is_go2rtc_arbitrary_exec_allowed() -> bool:
|
||||
"""Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker
|
||||
secrets, or the Home Assistant add-on options file."""
|
||||
raw: Optional[str] = None
|
||||
@@ -822,7 +822,7 @@ def is_restricted_go2rtc_source(stream_source: str) -> bool:
|
||||
and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set."""
|
||||
if not stream_source.strip().startswith(("echo:", "expr:", "exec:")):
|
||||
return False
|
||||
return not _go2rtc_arbitrary_exec_allowed()
|
||||
return not is_go2rtc_arbitrary_exec_allowed()
|
||||
|
||||
|
||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||
|
||||
Generated
+14
-4
@@ -52,7 +52,7 @@
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml": "^4.3.0",
|
||||
"konva": "^10.2.3",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -9407,9 +9407,19 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@
|
||||
"i18next-http-backend": "^3.0.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"immer": "^10.1.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"js-yaml": "^4.3.0",
|
||||
"konva": "^10.2.3",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Submitted"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Failed to submit to Frigate+. Please check your network connection and try again."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -493,6 +493,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximum concurrent exports",
|
||||
"description": "Maximum number of export jobs to process at the same time."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Chapter metadata to embed in exported recordings"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
|
||||
@@ -1035,6 +1035,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximum concurrent exports",
|
||||
"description": "Maximum number of export jobs to process at the same time."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Chapter metadata to embed in exported recordings"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
|
||||
@@ -1924,6 +1924,10 @@
|
||||
"resolutionHigh": "This detect resolution is higher than recommended and may cause increased resource usage without improving detection accuracy. A detect resolution at or below 1080p is recommended for most cameras.",
|
||||
"globalResolutionMultipleCameras": "A global detect resolution is set while multiple cameras are configured. Unless all cameras share the same resolution and aspect ratio, the detect width and height should be defined per camera to match each camera's native aspect ratio."
|
||||
},
|
||||
"model": {
|
||||
"optimizedFor320": "Frigate is optimized for a 320x320 model, which is the best choice for most setups. A 640x640 model is slower and only helps in specific scenarios.",
|
||||
"inputDimensionsNotDetectResolution": "Model input width and height are the input dimensions of the object detection model, not your camera's detect resolution. They should match the dimensions of the model you're using — typically a square size like 320x320 or 640x640."
|
||||
},
|
||||
"ffmpeg": {
|
||||
"hwaccelManualNotRecommended": "Manual hardware acceleration arguments are not recommended. Unless a specific requirement exists, select the preset that matches your hardware."
|
||||
},
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { LuInfo, LuTriangleAlert, LuCircleAlert } from "react-icons/lu";
|
||||
import {
|
||||
LuInfo,
|
||||
LuTriangleAlert,
|
||||
LuCircleAlert,
|
||||
LuExternalLink,
|
||||
} from "react-icons/lu";
|
||||
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||
import type { MessageSeverity } from "./section-configs/types";
|
||||
|
||||
const severityVariantMap: Record<
|
||||
@@ -28,13 +35,18 @@ function SeverityIcon({ severity }: { severity: string }) {
|
||||
type ConfigFieldMessageProps = {
|
||||
messageKey: string;
|
||||
severity: string;
|
||||
values?: Record<string, unknown>;
|
||||
docLink?: string;
|
||||
};
|
||||
|
||||
export function ConfigFieldMessage({
|
||||
messageKey,
|
||||
severity,
|
||||
values,
|
||||
docLink,
|
||||
}: ConfigFieldMessageProps) {
|
||||
const { t } = useTranslation("views/settings");
|
||||
const { t } = useTranslation(["views/settings", "common"]);
|
||||
const { getLocaleDocUrl } = useDocDomain();
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -42,7 +54,22 @@ export function ConfigFieldMessage({
|
||||
className="flex items-center [&>svg+div]:translate-y-0 [&>svg]:static [&>svg~*]:pl-2"
|
||||
>
|
||||
<SeverityIcon severity={severity} />
|
||||
<AlertDescription>{t(messageKey)}</AlertDescription>
|
||||
<AlertDescription>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>{t(messageKey, values)}</span>
|
||||
{docLink && (
|
||||
<Link
|
||||
to={getLocaleDocUrl(docLink)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center underline"
|
||||
>
|
||||
{t("readTheDocumentation", { ns: "common" })}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ const detect: SectionConfigOverrides = {
|
||||
messageKey: "configMessages.detect.fpsGreaterThanFive",
|
||||
severity: "info",
|
||||
position: "after",
|
||||
docLink: "/frigate/camera_setup#choosing-a-detect-frame-rate",
|
||||
condition: (ctx) => {
|
||||
if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
|
||||
if (ctx.fullCameraConfig.type === "lpr") return false;
|
||||
|
||||
@@ -3,6 +3,39 @@ import type { SectionConfigOverrides } from "./types";
|
||||
const model: SectionConfigOverrides = {
|
||||
base: {
|
||||
sectionDocs: "/configuration/object_detectors#model",
|
||||
fieldMessages: [
|
||||
{
|
||||
key: "model-optimized-for-320",
|
||||
field: "width",
|
||||
position: "before",
|
||||
messageKey: "configMessages.model.optimizedFor320",
|
||||
severity: "info",
|
||||
docLink: "/configuration/object_detectors#choosing-a-model-size",
|
||||
condition: (ctx) => {
|
||||
const width = ctx.formData?.width as number | null | undefined;
|
||||
const height = ctx.formData?.height as number | null | undefined;
|
||||
return width === 640 || height === 640;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "model-input-dimensions-not-detect-resolution",
|
||||
field: "height",
|
||||
position: "after",
|
||||
messageKey: "configMessages.model.inputDimensionsNotDetectResolution",
|
||||
severity: "warning",
|
||||
condition: (ctx) => {
|
||||
const width = ctx.formData?.width as number | null | undefined;
|
||||
const height = ctx.formData?.height as number | null | undefined;
|
||||
if (typeof width !== "number" || typeof height !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
return width > 640 || height > 640;
|
||||
},
|
||||
},
|
||||
],
|
||||
restartRequired: [
|
||||
"path",
|
||||
"labelmap_path",
|
||||
|
||||
@@ -26,6 +26,8 @@ export type ConditionalMessage = {
|
||||
condition: (ctx: MessageConditionContext) => boolean;
|
||||
/** Optional interpolation values passed to t() for {{var}} substitution */
|
||||
values?: Record<string, unknown>;
|
||||
/** Optional documentation path (e.g. "/configuration/object_detectors#model"). */
|
||||
docLink?: string;
|
||||
};
|
||||
|
||||
/** Field-level conditional message, adds field targeting */
|
||||
|
||||
@@ -406,6 +406,8 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
key={m.key}
|
||||
messageKey={m.messageKey}
|
||||
severity={m.severity}
|
||||
values={m.values}
|
||||
docLink={m.docLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -418,6 +420,8 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
key={m.key}
|
||||
messageKey={m.messageKey}
|
||||
severity={m.severity}
|
||||
values={m.values}
|
||||
docLink={m.docLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1251,30 +1251,42 @@ function ObjectDetailsTab({
|
||||
return;
|
||||
}
|
||||
|
||||
falsePositive
|
||||
? axios.put(`events/${search.id}/false_positive`)
|
||||
: axios.post(`events/${search.id}/plus`, {
|
||||
include_annotation: 1,
|
||||
});
|
||||
try {
|
||||
const resp = falsePositive
|
||||
? await axios.put(`events/${search.id}/false_positive`)
|
||||
: await axios.post(`events/${search.id}/plus`, {
|
||||
include_annotation: 1,
|
||||
});
|
||||
|
||||
setState("submitted");
|
||||
setSearch({ ...search, plus_id: "new_upload" });
|
||||
mutate(
|
||||
(key) => isEventsKey(key),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? { ...event, plus_id: "new_upload" }
|
||||
: event,
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
revalidate: false,
|
||||
},
|
||||
);
|
||||
if (resp.status !== 200 || !resp.data?.success) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
setState("submitted");
|
||||
setSearch({ ...search, plus_id: "new_upload" });
|
||||
mutate(
|
||||
(key) => isEventsKey(key),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) =>
|
||||
mapSearchResults(currentData, (event) =>
|
||||
event.id === search.id
|
||||
? { ...event, plus_id: "new_upload" }
|
||||
: event,
|
||||
),
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
revalidate: false,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
setState("reviewing");
|
||||
toast.error(
|
||||
t("explore.plus.review.toast.error", { ns: "components/dialog" }),
|
||||
{ position: "top-center" },
|
||||
);
|
||||
}
|
||||
},
|
||||
[search, mutate, mapSearchResults, setSearch, isEventsKey],
|
||||
[search, mutate, mapSearchResults, setSearch, isEventsKey, t],
|
||||
);
|
||||
|
||||
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -1225,11 +1225,15 @@ function LifecycleIconRow({
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onSelect={async () => {
|
||||
const resp = await axios.post(
|
||||
`/${item.camera}/plus/${item.timestamp + annotationOffset / 1000}`,
|
||||
);
|
||||
try {
|
||||
const resp = await axios.post(
|
||||
`/${item.camera}/plus/${item.timestamp + annotationOffset / 1000}`,
|
||||
);
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (resp && resp.status == 200) {
|
||||
toast.success(
|
||||
t("toast.success.submittedFrigatePlus", {
|
||||
ns: "components/player",
|
||||
@@ -1238,8 +1242,8 @@ function LifecycleIconRow({
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
} catch {
|
||||
toast.error(
|
||||
t("toast.error.submitFrigatePlusFailed", {
|
||||
ns: "components/player",
|
||||
}),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isDesktop, isMobile, isSafari } from "react-device-detect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
@@ -48,13 +49,28 @@ export function FrigatePlusDialog({
|
||||
const onSubmitToPlus = useCallback(
|
||||
async (falsePositive: boolean) => {
|
||||
if (!upload) return;
|
||||
falsePositive
|
||||
? axios.put(`events/${upload.id}/false_positive`)
|
||||
: axios.post(`events/${upload.id}/plus`, { include_annotation: 1 });
|
||||
setState("submitted");
|
||||
onEventUploaded();
|
||||
|
||||
try {
|
||||
const resp = falsePositive
|
||||
? await axios.put(`events/${upload.id}/false_positive`)
|
||||
: await axios.post(`events/${upload.id}/plus`, {
|
||||
include_annotation: 1,
|
||||
});
|
||||
|
||||
if (resp.status !== 200 || !resp.data?.success) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
setState("submitted");
|
||||
onEventUploaded();
|
||||
} catch {
|
||||
setState("reviewing");
|
||||
toast.error(t("explore.plus.review.toast.error"), {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
},
|
||||
[upload, onEventUploaded],
|
||||
[upload, onEventUploaded, t],
|
||||
);
|
||||
|
||||
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
|
||||
|
||||
@@ -5,7 +5,8 @@ import { LiveStreamMetadata } from "@/types/live";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 10000;
|
||||
const DEFER_DELAY_MS = 2000;
|
||||
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
|
||||
const emptyObject: Readonly<{ [key: string]: LiveStreamMetadata }> =
|
||||
Object.freeze({});
|
||||
|
||||
/**
|
||||
* Hook that fetches go2rtc stream metadata with deferred loading.
|
||||
@@ -78,7 +79,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
|
||||
return metadata;
|
||||
}, []);
|
||||
|
||||
const { data: metadata = EMPTY_METADATA } = useSWR<{
|
||||
const { data: metadata = emptyObject } = useSWR<{
|
||||
[key: string]: LiveStreamMetadata;
|
||||
}>(swrKey, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
|
||||
Reference in New Issue
Block a user