Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub f56be411f5 Merge 5d0d89a3f2 into 5003ab895c 2026-06-21 02:40:20 +08:00
dependabot[bot]andGitHub 5d0d89a3f2 Bump mermaid from 11.12.2 to 11.15.0 in /docs
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.12.2 to 11.15.0.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.2...mermaid@11.15.0)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.15.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 22:56:50 +00:00
55 changed files with 1845 additions and 1949 deletions
@@ -15,24 +15,12 @@ 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_go2rtc_arbitrary_exec_allowed,
is_restricted_go2rtc_source,
)
from frigate.util.services import 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:
@@ -112,7 +100,7 @@ for name in list(go2rtc_config.get("streams", {})):
if isinstance(stream, str):
try:
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
formatted_stream = substitute_frigate_vars(stream)
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. "
@@ -131,7 +119,7 @@ for name in list(go2rtc_config.get("streams", {})):
filtered_streams = []
for i, stream_item in enumerate(stream):
try:
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
formatted_stream = substitute_frigate_vars(stream_item)
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. "
@@ -155,20 +143,6 @@ 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")
-349
View File
@@ -1,349 +0,0 @@
{
"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) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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"
}
]
}
}
+2 -2
View File
@@ -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: `750` pixels
- Default: `500` 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: 750
min_area: 500
```
</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](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected.
- Watch the debug view (Settings --> Debug) 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.
+1 -23
View File
@@ -7,8 +7,6 @@ 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._
@@ -23,16 +21,7 @@ Object filter masks can be used to filter out stubborn false positives in fixed
![object mask](/img/bottom-center-mask.jpg)
## 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
## Creating masks
<ConfigTabs>
<TabItem value="ui">
@@ -135,14 +124,3 @@ 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.
-2
View File
@@ -59,8 +59,6 @@ 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
+4 -60
View File
@@ -199,6 +199,10 @@ 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.
@@ -351,63 +355,3 @@ 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.
+4 -4
View File
@@ -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#amdrocm-gpu-detector)
- [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models)
- 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-detector)
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models)
- 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-detector)
- [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models)
- 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)
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models)
- 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.
-2
View File
@@ -42,8 +42,6 @@ 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.
-16
View File
@@ -124,19 +124,3 @@ 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.
-6
View File
@@ -9,12 +9,6 @@ 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.
+1 -1
View File
@@ -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. 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.
- **Timeline** — a scrubbable vertical timeline of the selected camera, annotated with a motion line, review-item markers, and gaps where 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.
+2 -2
View File
@@ -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 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).
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).
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 camera 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 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.
+56 -159
View File
@@ -2072,43 +2072,10 @@
"integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==",
"license": "MIT"
},
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
"integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/gast": "11.0.3",
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/@chevrotain/gast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz",
"integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/types": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/@chevrotain/regexp-to-ast": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz",
"integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/types": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz",
"integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==",
"license": "Apache-2.0"
},
"node_modules/@chevrotain/utils": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz",
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
"license": "Apache-2.0"
},
"node_modules/@colors/colors": {
@@ -4504,12 +4471,12 @@
}
},
"node_modules/@mermaid-js/parser": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz",
"integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz",
"integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==",
"license": "MIT",
"dependencies": {
"langium": "3.3.1"
"@chevrotain/types": "~11.1.1"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -5995,6 +5962,16 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@upsetjs/venn.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
"integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
"license": "MIT",
"optionalDependencies": {
"d3-selection": "^3.0.0",
"d3-transition": "^3.0.1"
}
},
"node_modules/@vercel/oidc": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
@@ -7199,32 +7176,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/chevrotain": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz",
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"license": "Apache-2.0",
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
"@chevrotain/regexp-to-ast": "11.0.3",
"@chevrotain/types": "11.0.3",
"@chevrotain/utils": "11.0.3",
"lodash-es": "4.17.21"
}
},
"node_modules/chevrotain-allstar": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz",
"integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==",
"license": "MIT",
"dependencies": {
"lodash-es": "^4.17.21"
},
"peerDependencies": {
"chevrotain": "^11.0.0"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -8560,9 +8511,9 @@
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
"license": "ISC",
"engines": {
"node": ">=12"
@@ -8796,9 +8747,9 @@
}
},
"node_modules/dagre-d3-es": {
"version": "7.0.13",
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz",
"integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==",
"version": "7.0.14",
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
"integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
"license": "MIT",
"dependencies": {
"d3": "^7.9.0",
@@ -8973,9 +8924,9 @@
}
},
"node_modules/delaunator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
"integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
"license": "ISC",
"dependencies": {
"robust-predicates": "^3.0.2"
@@ -10499,6 +10450,16 @@
"node": ">= 0.4"
}
},
"node_modules/es-toolkit": {
"version": "1.46.1",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz",
"integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/es6-promise": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@@ -13058,22 +13019,6 @@
"node": ">=6"
}
},
"node_modules/langium": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz",
"integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==",
"license": "MIT",
"dependencies": {
"chevrotain": "~11.0.3",
"chevrotain-allstar": "~0.3.0",
"vscode-languageserver": "~9.0.1",
"vscode-languageserver-textdocument": "~1.0.11",
"vscode-uri": "~3.0.8"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/latest-version": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz",
@@ -13190,9 +13135,9 @@
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
@@ -13840,31 +13785,32 @@
}
},
"node_modules/mermaid": {
"version": "11.12.2",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz",
"integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==",
"version": "11.15.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz",
"integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.1.1",
"@iconify/utils": "^3.0.1",
"@mermaid-js/parser": "^0.6.3",
"@iconify/utils": "^3.0.2",
"@mermaid-js/parser": "^1.1.1",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.3",
"@upsetjs/venn.js": "^2.0.0",
"cytoscape": "^3.33.1",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.13",
"dayjs": "^1.11.18",
"dompurify": "^3.2.5",
"katex": "^0.16.22",
"dagre-d3-es": "7.0.14",
"dayjs": "^1.11.19",
"dompurify": "^3.3.1",
"es-toolkit": "^1.45.1",
"katex": "^0.16.25",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^16.2.1",
"marked": "^16.3.0",
"roughjs": "^4.6.6",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^11.1.0"
"uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0"
}
},
"node_modules/methods": {
@@ -20210,9 +20156,9 @@
}
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
"integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
"license": "Unlicense"
},
"node_modules/roughjs": {
@@ -22561,55 +22507,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageserver": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
"integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
"license": "MIT",
"dependencies": {
"vscode-languageserver-protocol": "3.17.5"
},
"bin": {
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
"license": "MIT"
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"license": "MIT"
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
-1
View File
@@ -29,7 +29,6 @@
"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",
@@ -1,171 +0,0 @@
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>
);
}
@@ -1,275 +0,0 @@
/* ===================================================================
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);
}
-14
View File
@@ -7393,13 +7393,6 @@ components:
required:
- value
title: CameraSetBody
ChaptersEnum:
type: string
enum:
- none
- recording_segments
- review_items
title: ChaptersEnum
ChatCompletionRequest:
properties:
messages:
@@ -8126,13 +8119,6 @@ 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:
-13
View File
@@ -147,19 +147,6 @@ 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,10 +3,7 @@ from typing import Optional, Union
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema
from frigate.record.export import (
ChaptersEnum,
PlaybackSourceEnum,
)
from frigate.record.export import PlaybackSourceEnum
class ExportRecordingsBody(BaseModel):
@@ -21,14 +18,6 @@ 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):
-24
View File
@@ -68,7 +68,6 @@ 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,
)
@@ -129,15 +128,6 @@ 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):
@@ -264,7 +254,6 @@ 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),
@@ -278,7 +267,6 @@ def _build_export_job(
ffmpeg_input_args=ffmpeg_input_args,
ffmpeg_output_args=ffmpeg_output_args,
cpu_fallback=cpu_fallback,
chapters=chapters,
)
@@ -737,9 +725,6 @@ 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)
@@ -818,14 +803,6 @@ 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.
@@ -862,7 +839,6 @@ def export_recording(
existing_image,
playback_source,
export_case_id,
chapters=chapters,
)
try:
start_export_job(request.app.frigate_config, export_job)
+13 -32
View File
@@ -60,19 +60,6 @@ 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,
@@ -426,9 +413,7 @@ async def submit_recording_snapshot_to_plus(
)
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
await asyncio.to_thread(
request.app.frigate_config.plus_api.upload_image, nd, camera_name
)
request.app.frigate_config.plus_api.upload_image(nd, camera_name)
return JSONResponse(
content={
@@ -951,7 +936,7 @@ async def event_thumbnail(
thumbnail_bytes,
media_type=extension.get_mime_type(),
headers={
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}"
"Cache-Control": f"private, max-age={max_cache_age}"
if event_complete
else "no-store",
},
@@ -1285,14 +1270,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 await preview_gif(request, event.camera, start_ts, end_ts)
return 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)],
)
async def preview_gif(
def preview_gif(
request: Request,
camera_name: str,
start_ts: float,
@@ -1355,8 +1340,7 @@ async def preview_gif(
"-",
]
process = await asyncio.to_thread(
sp.run,
process = sp.run(
ffmpeg_cmd,
capture_output=True,
)
@@ -1435,8 +1419,7 @@ async def preview_gif(
"-",
]
process = await asyncio.to_thread(
sp.run,
process = sp.run(
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1455,7 +1438,7 @@ async def preview_gif(
gif_bytes,
media_type="image/gif",
headers={
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Cache-Control": f"private, max-age={max_cache_age}",
"Content-Type": "image/gif",
},
)
@@ -1465,7 +1448,7 @@ async def preview_gif(
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4",
dependencies=[Depends(require_camera_access)],
)
async def preview_mp4(
def preview_mp4(
request: Request,
camera_name: str,
start_ts: float,
@@ -1545,8 +1528,7 @@ async def preview_mp4(
path,
]
process = await asyncio.to_thread(
sp.run,
process = sp.run(
ffmpeg_cmd,
capture_output=True,
)
@@ -1622,8 +1604,7 @@ async def preview_mp4(
path,
]
process = await asyncio.to_thread(
sp.run,
process = sp.run(
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1638,7 +1619,7 @@ async def preview_mp4(
headers = {
"Content-Description": "File Transfer",
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Cache-Control": f"private, max-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
@@ -1676,9 +1657,9 @@ async def review_preview(
)
if format == "gif":
return await preview_gif(request, review.camera, start_ts, end_ts)
return preview_gif(request, review.camera, start_ts, end_ts)
else:
return await preview_mp4(request, review.camera, start_ts, end_ts)
return preview_mp4(request, review.camera, start_ts, end_ts)
@router.get(
+16 -36
View File
@@ -1,9 +1,7 @@
"""Preview apis."""
import bisect
import logging
import os
import threading
from datetime import datetime, timedelta, timezone
import pytz
@@ -135,32 +133,6 @@ def preview_hour(
return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
# cache one sorted listing of the shared preview_frames dir
_preview_listing_lock = threading.Lock()
_preview_listing_cache: tuple[float, list[str]] = (-1.0, [])
def _get_preview_frame_listing(preview_dir: str) -> list[str]:
"""Return the sorted preview_frames listing, cached until the dir changes."""
global _preview_listing_cache
# mtime bumps when a frame is added or removed, invalidating the cache
mtime = os.stat(preview_dir).st_mtime
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
with _preview_listing_lock:
# another thread may have refreshed the cache while we waited
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
files = sorted(entry.name for entry in os.scandir(preview_dir))
_preview_listing_cache = (mtime, files)
return files
@router.get(
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames",
response_model=PreviewFramesResponse,
@@ -177,15 +149,23 @@ def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: flo
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
files = _get_preview_frame_listing(preview_dir)
# a camera's frames form a contiguous slice of the sorted listing;
# bisect locates it without scanning the whole directory
left = bisect.bisect_left(files, start_file)
right = bisect.bisect_right(files, end_file)
selected_previews = [
file for file in files[left:right] if file.startswith(file_start)
camera_files = [
entry.name
for entry in os.scandir(preview_dir)
if entry.name.startswith(file_start)
]
camera_files.sort()
selected_previews = []
for file in camera_files:
if file < start_file:
continue
if file > end_file:
break
selected_previews.append(file)
return JSONResponse(
content=selected_previews,
+7 -39
View File
@@ -72,16 +72,11 @@ _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.
@@ -89,10 +84,6 @@ 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.
@@ -102,33 +93,16 @@ def _check_ws_authorization(
return False
# No role header: default to viewer (fail-closed)
roles = [r.strip() for r in role_header.split(separator)] if role_header else []
if role_header is None:
return topic in _WS_VIEWER_TOPICS
# Admin can send anything
# Check if any role is admin
roles = [r.strip() for r in role_header.split(separator)]
if "admin" in roles:
return True
# 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
# Non-admin: only viewer topics allowed
return topic in _WS_VIEWER_TOPICS
# ---- Outbound filtering ---------------------------------------------------
@@ -475,8 +449,6 @@ 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:
@@ -498,11 +470,7 @@ 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,
self.roles_config,
self.camera_names,
topic, role_header, self.role_separator
):
logger.warning(
"Blocked unauthorized WebSocket message: topic=%s, role=%s",
-11
View File
@@ -9,7 +9,6 @@ from frigate.review.types import SeverityEnum
from ..base import FrigateBaseModel
__all__ = [
"ChaptersEnum",
"RecordConfig",
"RecordExportConfig",
"RecordPreviewConfig",
@@ -87,12 +86,6 @@ 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",
@@ -105,10 +98,6 @@ 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):
-3
View File
@@ -13,7 +13,6 @@ 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
@@ -56,7 +55,6 @@ 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
@@ -345,7 +343,6 @@ class ExportJobManager:
job.ffmpeg_input_args,
job.ffmpeg_output_args,
job.cpu_fallback,
job.chapters,
on_progress=self._make_progress_callback(job),
)
+5 -113
View File
@@ -17,7 +17,6 @@ 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,
@@ -218,7 +217,6 @@ 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__()
@@ -234,7 +232,6 @@ 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
@@ -512,74 +509,6 @@ 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")
@@ -743,18 +672,7 @@ class RecordingExporter(threading.Thread):
)
).split(" ")
else:
# 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
chapters_path = self._build_chapter_metadata_file(recordings)
chapter_args = (
f" -i {chapters_path} -map 0 -dn -map_metadata 1"
if chapters_path
@@ -766,19 +684,7 @@ 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)}"
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.extend(["-metadata", f"title={title}"])
ffmpeg_cmd.append(video_path)
@@ -864,32 +770,18 @@ 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".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
EncodeTypeEnum.timelapse,
)
).split(" ")
else:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
).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)}"
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)
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
return ffmpeg_cmd, playlist_lines
-21
View File
@@ -42,8 +42,6 @@ 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__(
@@ -303,8 +301,6 @@ 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
)
@@ -315,21 +311,6 @@ 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)
@@ -650,8 +631,6 @@ 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,
-41
View File
@@ -1,41 +0,0 @@
"""Tests for frigate.util.builtin helpers."""
import unittest
from unittest.mock import patch
from frigate.util.builtin import EventsPerSecond
class TestEventsPerSecond(unittest.TestCase):
def test_eps_is_zero_before_any_events(self) -> None:
eps = EventsPerSecond()
with patch("frigate.util.builtin.time.monotonic", return_value=100.0):
self.assertEqual(eps.eps(), 0.0)
def test_eps_counts_events_in_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [1000.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
# one event per second for five seconds
for _ in range(5):
clock[0] += 1.0
eps.update()
# five events over the five seconds since start
self.assertAlmostEqual(eps.eps(), 1.0)
def test_old_timestamps_expire_from_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [0.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
for _ in range(10):
clock[0] += 1.0
eps.update()
# jump well past the window so every timestamp ages out
clock[0] += 100.0
self.assertEqual(eps.eps(), 0.0)
if __name__ == "__main__":
unittest.main()
-40
View File
@@ -115,46 +115,6 @@ 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()
-16
View File
@@ -1,22 +1,6 @@
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):
@@ -1,39 +0,0 @@
"""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()
-18
View File
@@ -82,24 +82,6 @@ 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")
-128
View File
@@ -11,16 +11,6 @@ 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):
@@ -171,124 +161,6 @@ 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()
+2 -4
View File
@@ -641,11 +641,9 @@ 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 = {
tuple(obj["box"]) for obj in self.tracked_objects.values()
}
tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()]
self.untracked_object_boxes = [
o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes
o[2] for o in detections if o[2] not in tracked_object_boxes
]
def print_objects_as_table(self, tracked_objects: Sequence) -> None:
+1 -4
View File
@@ -63,9 +63,6 @@ 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."""
@@ -79,7 +76,7 @@ def get_stationary_threshold(label: str) -> StationaryThresholds:
if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects:
return NON_STATIONARY_OBJECT_THRESHOLDS
return DEFAULT_OBJECT_THRESHOLDS
return StationaryThresholds()
class StationaryMotionClassifier:
+9 -7
View File
@@ -2,6 +2,7 @@
import ast
import copy
import datetime
import logging
import math
import multiprocessing.queues
@@ -9,9 +10,7 @@ import queue
import re
import shlex
import struct
import time
import urllib.parse
from collections import deque
from collections.abc import Mapping
from multiprocessing.managers import ValueProxy
from pathlib import Path
@@ -33,20 +32,23 @@ class EventsPerSecond:
self._start = None
self._max_events = max_events
self._last_n_seconds = last_n_seconds
self._timestamps: deque[float] = deque(maxlen=max_events)
self._timestamps = []
def start(self) -> None:
self._start = time.monotonic()
self._start = datetime.datetime.now().timestamp()
def update(self) -> None:
now = time.monotonic()
now = datetime.datetime.now().timestamp()
if self._start is None:
self._start = now
self._timestamps.append(now)
# truncate the list when it goes 100 over the max_size
if len(self._timestamps) > self._max_events + 100:
self._timestamps = self._timestamps[(1 - self._max_events) :]
self.expire_timestamps(now)
def eps(self) -> float:
now = time.monotonic()
now = datetime.datetime.now().timestamp()
if self._start is None:
self._start = now
# compute the (approximate) events in the last n seconds
@@ -61,7 +63,7 @@ class EventsPerSecond:
def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold:
self._timestamps.popleft()
del self._timestamps[0]
class InferenceSpeed:
+19 -15
View File
@@ -339,13 +339,18 @@ 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."""
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,
]
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)]
def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]:
@@ -396,13 +401,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 = set()
used_boxes = []
# loop over each box
for current_index, b in enumerate(boxes):
if current_index in used_boxes:
continue
cluster = [current_index]
used_boxes.add(current_index)
used_boxes.append(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):
@@ -431,7 +436,7 @@ def get_cluster_candidates(frame_shape, min_region, boxes):
if should_cluster:
cluster.append(compare_index)
used_boxes.add(compare_index)
used_boxes.append(compare_index)
cluster_candidates.append(cluster)
# return the unique clusters only
@@ -553,7 +558,6 @@ 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)),
@@ -564,14 +568,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 current_area / area(to_check) < 0.05:
if area(current_box) / 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
) / current_area > LABEL_CONSOLIDATION_MAP.get(
if intersect_box is not None and area(intersect_box) / area(
current_box
) > LABEL_CONSOLIDATION_MAP.get(
current_label, LABEL_CONSOLIDATION_DEFAULT
):
overlap = 1
+2 -2
View File
@@ -790,7 +790,7 @@ def get_hailo_temps() -> dict[str, float]:
return temps
def is_go2rtc_arbitrary_exec_allowed() -> bool:
def _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 is_go2rtc_arbitrary_exec_allowed()
return not _go2rtc_arbitrary_exec_allowed()
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
+4 -14
View File
@@ -52,7 +52,7 @@
"i18next-http-backend": "^3.0.1",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1",
"js-yaml": "^4.3.0",
"js-yaml": "^4.1.1",
"konva": "^10.2.3",
"lodash": "^4.18.1",
"lucide-react": "^0.577.0",
@@ -9407,19 +9407,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/js-yaml": {
"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"
}
],
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
+1 -1
View File
@@ -66,7 +66,7 @@
"i18next-http-backend": "^3.0.1",
"idb-keyval": "^6.2.1",
"immer": "^10.1.1",
"js-yaml": "^4.3.0",
"js-yaml": "^4.1.1",
"konva": "^10.2.3",
"lodash": "^4.18.1",
"lucide-react": "^0.577.0",
@@ -24,9 +24,6 @@
},
"state": {
"submitted": "Submitted"
},
"toast": {
"error": "Failed to submit to Frigate+. Please check your network connection and try again."
}
}
},
@@ -493,9 +493,6 @@
"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": {
-3
View File
@@ -1035,9 +1035,6 @@
"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,10 +1924,6 @@
"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,13 +1,6 @@
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
LuInfo,
LuTriangleAlert,
LuCircleAlert,
LuExternalLink,
} from "react-icons/lu";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { LuInfo, LuTriangleAlert, LuCircleAlert } from "react-icons/lu";
import type { MessageSeverity } from "./section-configs/types";
const severityVariantMap: Record<
@@ -35,18 +28,13 @@ 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", "common"]);
const { getLocaleDocUrl } = useDocDomain();
const { t } = useTranslation("views/settings");
return (
<Alert
@@ -54,22 +42,7 @@ export function ConfigFieldMessage({
className="flex items-center [&>svg+div]:translate-y-0 [&>svg]:static [&>svg~*]:pl-2"
>
<SeverityIcon severity={severity} />
<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>
<AlertDescription>{t(messageKey)}</AlertDescription>
</Alert>
);
}
@@ -116,7 +116,6 @@ 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,39 +3,6 @@ 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,8 +26,6 @@ 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,8 +406,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
key={m.key}
messageKey={m.messageKey}
severity={m.severity}
values={m.values}
docLink={m.docLink}
/>
))}
</div>
@@ -420,8 +418,6 @@ export function FieldTemplate(props: FieldTemplateProps) {
key={m.key}
messageKey={m.messageKey}
severity={m.severity}
values={m.values}
docLink={m.docLink}
/>
))}
</div>
@@ -1251,42 +1251,30 @@ function ObjectDetailsTab({
return;
}
try {
const resp = falsePositive
? await axios.put(`events/${search.id}/false_positive`)
: await axios.post(`events/${search.id}/plus`, {
include_annotation: 1,
});
falsePositive
? axios.put(`events/${search.id}/false_positive`)
: axios.post(`events/${search.id}/plus`, {
include_annotation: 1,
});
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" },
);
}
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,
},
);
},
[search, mutate, mapSearchResults, setSearch, isEventsKey, t],
[search, mutate, mapSearchResults, setSearch, isEventsKey],
);
const popoverContainerRef = useRef<HTMLDivElement | null>(null);
@@ -1225,15 +1225,11 @@ function LifecycleIconRow({
<DropdownMenuItem
className="cursor-pointer"
onSelect={async () => {
try {
const resp = await axios.post(
`/${item.camera}/plus/${item.timestamp + annotationOffset / 1000}`,
);
if (resp.status !== 200) {
throw new Error();
}
const resp = await axios.post(
`/${item.camera}/plus/${item.timestamp + annotationOffset / 1000}`,
);
if (resp && resp.status == 200) {
toast.success(
t("toast.success.submittedFrigatePlus", {
ns: "components/player",
@@ -1242,8 +1238,8 @@ function LifecycleIconRow({
position: "top-center",
},
);
} catch {
toast.error(
} else {
toast.success(
t("toast.error.submitFrigatePlusFailed", {
ns: "components/player",
}),
@@ -10,7 +10,6 @@ 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";
@@ -49,28 +48,13 @@ export function FrigatePlusDialog({
const onSubmitToPlus = useCallback(
async (falsePositive: boolean) => {
if (!upload) return;
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",
});
}
falsePositive
? axios.put(`events/${upload.id}/false_positive`)
: axios.post(`events/${upload.id}/plus`, { include_annotation: 1 });
setState("submitted");
onEventUploaded();
},
[upload, onEventUploaded, t],
[upload, onEventUploaded],
);
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
@@ -5,8 +5,7 @@ import { LiveStreamMetadata } from "@/types/live";
const FETCH_TIMEOUT_MS = 10000;
const DEFER_DELAY_MS = 2000;
const emptyObject: Readonly<{ [key: string]: LiveStreamMetadata }> =
Object.freeze({});
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
/**
* Hook that fetches go2rtc stream metadata with deferred loading.
@@ -79,7 +78,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
return metadata;
}, []);
const { data: metadata = emptyObject } = useSWR<{
const { data: metadata = EMPTY_METADATA } = useSWR<{
[key: string]: LiveStreamMetadata;
}>(swrKey, fetcher, {
revalidateOnFocus: false,