diff --git a/.github/DISCUSSION_TEMPLATE/beta-support.yml b/.github/DISCUSSION_TEMPLATE/beta-support.yml index e342127a02..bad899fe03 100644 --- a/.github/DISCUSSION_TEMPLATE/beta-support.yml +++ b/.github/DISCUSSION_TEMPLATE/beta-support.yml @@ -10,7 +10,8 @@ body: Before submitting, read the [beta documentation][docs]. - [docs]: https://deploy-preview-19787--frigate-docs.netlify.app/ + [docs]: https://docs-dev.frigate.video/ + [discussions]: https://github.com/blakeblackshear/frigate/discussions - type: textarea id: description attributes: @@ -22,8 +23,8 @@ body: id: version attributes: label: Beta Version - description: Visible on the System page in the Web UI. Please include the full version including the build identifier (eg. 0.17.0-beta1) - placeholder: "0.17.0-beta1" + description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.18.0-beta1, 0.18.0-8b72c7a, etc.) + placeholder: "0.18.0-beta1" validations: required: true - type: dropdown @@ -71,11 +72,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxomox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/camera-support.yml b/.github/DISCUSSION_TEMPLATE/camera-support.yml index 521d65ded5..2c06492a8c 100644 --- a/.github/DISCUSSION_TEMPLATE/camera-support.yml +++ b/.github/DISCUSSION_TEMPLATE/camera-support.yml @@ -87,11 +87,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxomox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/config-support.yml b/.github/DISCUSSION_TEMPLATE/config-support.yml index 575f7f640e..564c9083b9 100644 --- a/.github/DISCUSSION_TEMPLATE/config-support.yml +++ b/.github/DISCUSSION_TEMPLATE/config-support.yml @@ -73,11 +73,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxomox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/detector-support.yml b/.github/DISCUSSION_TEMPLATE/detector-support.yml index fb994500f4..12c806b5de 100644 --- a/.github/DISCUSSION_TEMPLATE/detector-support.yml +++ b/.github/DISCUSSION_TEMPLATE/detector-support.yml @@ -53,11 +53,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxomox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/general-support.yml b/.github/DISCUSSION_TEMPLATE/general-support.yml index 0b9f225b6b..78d1f8ba0c 100644 --- a/.github/DISCUSSION_TEMPLATE/general-support.yml +++ b/.github/DISCUSSION_TEMPLATE/general-support.yml @@ -73,11 +73,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxmox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml index 861156696a..de8d216874 100644 --- a/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml +++ b/.github/DISCUSSION_TEMPLATE/hardware-acceleration-support.yml @@ -69,11 +69,12 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI - Proxmox via Docker - - Proxmox via TTeck Script + - Proxmox via installation script + - Proxomox via VM - Windows WSL2 validations: required: true diff --git a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml index de870ac0f7..012d4ea663 100644 --- a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml +++ b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -6,7 +6,7 @@ body: value: | Use this form to submit a reproducible bug in Frigate or Frigate's UI. - **⚠️ If you are running a beta version (0.17.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.** + **⚠️ If you are running a beta version (0.18.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.** Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. @@ -116,9 +116,13 @@ body: attributes: label: Install method options: - - Home Assistant Add-on + - Home Assistant App - Docker Compose - Docker CLI + - Proxmox via Docker + - Proxmox via installation script + - Proxomox via VM + - Windows WSL2 validations: required: true - type: dropdown diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 516b55e89b..d2e279966e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -125,5 +125,7 @@ jobs: run: devcontainer up --workspace-folder . - name: Run mypy in devcontainer run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m mypy --config-file frigate/mypy.ini frigate" + - name: Check API spec is up to date + run: devcontainer exec --workspace-folder . bash -lc "python3 generate_api_auth_spec.py --check" - name: Run unit tests in devcontainer run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest" diff --git a/.gitignore b/.gitignore index 7c97a23a0e..70ec5ae76f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ config/* models *.mp4 *.db +*.db-* *.csv frigate/version.py web/build diff --git a/AGENTS.md b/AGENTS.md index 61b8373a82..41d4b6460b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ When reviewing code, do NOT comment on: - **Type Checking**: Use type hints consistently - **Testing**: unittest framework - use `python3 -u -m unittest` to run tests - **Language**: American English for all code, comments, and documentation +- **Punctuation**: Do not use em dashes in documentation, comments, or strings; reword with standard punctuation (commas, colons, parentheses, or separate sentences) ### Logging Standards @@ -235,6 +236,14 @@ ruff check frigate/ # Type check python3 -u -m mypy --config-file frigate/mypy.ini frigate + +# Regenerate the OpenAPI spec after adding, changing, or removing an API +# endpoint or its auth dependency — outputs docs/static/frigate-api.yaml, +# annotated with each endpoint's auth requirement (admin / any / camera / +# public). NEVER edit that file by hand. CI runs the --check variant and fails +# if it is out of date. (from repo root) +python3 generate_api_auth_spec.py +python3 generate_api_auth_spec.py --check ``` ### Frontend (from web/ directory) @@ -316,6 +325,8 @@ async def get_events(request: Request, limit: int = 100): # Implementation ``` +After adding, changing, or removing an endpoint (or its auth dependency), regenerate the OpenAPI spec with `python3 generate_api_auth_spec.py` so `docs/static/frigate-api.yaml` stays in sync and the endpoint's auth requirement is documented. CI enforces this via the `--check` variant; never edit that file by hand. + ### Configuration Access ```python diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index 1a475a650c..3ba1772f41 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -63,7 +63,7 @@ RUN --mount=type=bind,source=docker/main/build_intel_media_driver.sh,target=/dep FROM scratch AS go2rtc ARG TARGETARCH WORKDIR /rootfs/usr/local/go2rtc/bin -ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.13/go2rtc_linux_${TARGETARCH}" go2rtc +ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.14/go2rtc_linux_${TARGETARCH}" go2rtc FROM wget AS tempio ARG TARGETARCH diff --git a/docker/main/requirements-dev.txt b/docker/main/requirements-dev.txt index ac9d357583..df5818fe0d 100644 --- a/docker/main/requirements-dev.txt +++ b/docker/main/requirements-dev.txt @@ -1,4 +1,4 @@ -ruff +ruff == 0.15.20 # types types-peewee == 3.17.* diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 2b0fe3c925..70cb744f13 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -3,6 +3,7 @@ import json import os import sys +from pathlib import Path from typing import Any from ruamel.yaml import YAML @@ -15,12 +16,24 @@ from frigate.const import ( ) from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode from frigate.util.config import find_config_file, resolve_ffmpeg_path -from frigate.util.services import is_restricted_go2rtc_source +from frigate.util.services import ( + is_go2rtc_arbitrary_exec_allowed, + is_restricted_go2rtc_source, +) sys.path.remove("/opt/frigate") yaml = YAML() +FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} +# read docker secret files as env vars too +if os.path.isdir("/run/secrets"): + for secret_file in os.listdir("/run/secrets"): + if secret_file.startswith("FRIGATE_"): + FRIGATE_ENV_VARS[secret_file] = ( + Path(os.path.join("/run/secrets", secret_file)).read_text().strip() + ) + config_file = find_config_file() try: @@ -100,7 +113,7 @@ for name in list(go2rtc_config.get("streams", {})): if isinstance(stream, str): try: - formatted_stream = substitute_frigate_vars(stream) + formatted_stream = stream.format(**FRIGATE_ENV_VARS) if is_restricted_go2rtc_source(formatted_stream): print( f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. " @@ -119,7 +132,7 @@ for name in list(go2rtc_config.get("streams", {})): filtered_streams = [] for i, stream_item in enumerate(stream): try: - formatted_stream = substitute_frigate_vars(stream_item) + formatted_stream = stream_item.format(**FRIGATE_ENV_VARS) if is_restricted_go2rtc_source(formatted_stream): print( f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. " @@ -143,6 +156,20 @@ for name in list(go2rtc_config.get("streams", {})): ) del go2rtc_config["streams"][name] + elif isinstance(stream, dict): + # The map form ({"url": ...}) lets go2rtc resolve the source + # recursively, so it is effectively a dynamic way to generate the URL + # for a stream. That can only be backed by an exec source, so it cannot + # be allowed unless arbitrary exec is explicitly enabled. When it is + # enabled, leave the map untouched for go2rtc to resolve. + if not is_go2rtc_arbitrary_exec_allowed(): + print( + f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. " + f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources." + ) + del go2rtc_config["streams"][name] + continue + # add birdseye restream stream if enabled if config.get("birdseye", {}).get("restream", False): birdseye: dict[str, Any] = config.get("birdseye") diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index d0b18ff805..399dba5736 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -274,6 +274,13 @@ http { include proxy.conf; } + location /api/logout { + auth_request off; + rewrite ^/api(/.*)$ $1 break; + proxy_pass http://frigate_api; + include proxy.conf; + } + # Allow unauthenticated access to the first_time_login endpoint # so the login page can load help text before authentication. location /api/auth/first_time_login { diff --git a/docker/rockchip/conv2rknn.py b/docker/rockchip/conv2rknn.py index 4880d98684..700f35689f 100644 --- a/docker/rockchip/conv2rknn.py +++ b/docker/rockchip/conv2rknn.py @@ -11,10 +11,10 @@ except FileNotFoundError: pass try: - with open("/config/conv2rknn.yaml", "r") as config_file: + with open("/config/conv2rknn.yaml") as config_file: configuration = yaml.safe_load(config_file) except FileNotFoundError: - raise Exception("Please place a config file at /config/conv2rknn.yaml") + raise Exception("Please place a config file at /config/conv2rknn.yaml") from None if configuration["config"] != None: rknn_config = configuration["config"] @@ -31,7 +31,7 @@ if "soc" not in configuration: with open("/proc/device-tree/compatible") as file: soc = file.read().split(",")[-1].strip("\x00") except FileNotFoundError: - raise Exception("Make sure to run docker in privileged mode.") + raise Exception("Make sure to run docker in privileged mode.") from None configuration["soc"] = [ soc, diff --git a/docs/data/object_detectors_models.yaml b/docs/data/object_detectors_models.yaml new file mode 100644 index 0000000000..a4458c3091 --- /dev/null +++ b/docs/data/object_detectors_models.yaml @@ -0,0 +1,1346 @@ +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: + coral: + type: edgetpu + 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: + + | Field | Value | + | ---------------------------------------- | ----------------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` | + | **Label map for custom object detector** | `/config/labels-coco17.txt` | + | **Object detection model input width** | `320` (should match the imgsize of the model) | + | **Object detection model input height** | `320` (should match the imgsize of the model) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + coral: + type: edgetpu + device: usb + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize of the model, typically 320 + height: 320 # <--- should match the imgsize of the model, typically 320 + path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite + labelmap_path: /config/labels-coco17.txt +hailo8l: + title: Hailo-8/Hailo-8L + 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: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` | + | **Object Detection Model Type** | `yolo-generic` | + + The detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override. + yaml: |- + detectors: + hailo: + type: hailo8l + device: PCIe + + model: + width: 320 + height: 320 + input_tensor: nhwc + input_pixel_format: rgb + input_dtype: int + model_type: yolo-generic + labelmap_path: /labelmap/coco-80.txt + + # The detector automatically selects the default model based on your hardware: + # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef) + # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef) + # + # Optionally, you can specify a local model path to override the default. + # If a local path is provided and the file exists, it will be used instead of downloading. + # Example: + # path: /config/model_cache/hailo/yolov6n.hef + # + # You can also override using a custom URL: + # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef + # 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: + + | Field | Value | + | --------------------------------------- | ------ | + | **Object detection model input width** | `300` | + | **Object detection model input height** | `300` | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` | + + Specify the local model path or URL for SSD MobileNet v1. + yaml: |- + detectors: + hailo: + type: hailo8l + device: PCIe + + model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: rgb + model_type: ssd + # Specify the local model path (if available) or URL for SSD MobileNet v1. + # Example with a local path: + # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef + # + # Or override using a custom URL: + # 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: 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`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN 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 + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # or NPU + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: ssd + label: SSDLite MobileNet v2 + recommended: false + 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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------ | + | **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` | + | **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` | + | **Object detection model input width** | `300` | + | **Object detection model input height** | `300` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # Or NPU + + model: + width: 300 + height: 300 + input_tensor: nhwc + input_pixel_format: bgr + path: /openvino-model/ssdlite_mobilenet_v2.xml + labelmap_path: /openvino-model/coco_91cl_bkgr.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU # or NPU + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + 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). + + :::warning + + 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 + + ::: + + The 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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Custom object detector model path** | `/config/yolo_nas_s.onnx` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match whatever was set in notebook) | + | **Object detection model input height** | `320` (should match whatever was set in notebook) | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_tensor: nchw + input_pixel_format: bgr + path: /config/yolo_nas_s.onnx + 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: + + | Field | Value | + | ------------------------------------- | -------------------------------- | + | **Custom object detector model path** | `/config/yolox.onnx` (use the filename you generated above) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: yolox + path: /config/model_cache/yolox.onnx # use the filename you generated above + 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. + + ```sh + docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF' + FROM python:3.12 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /rfdetr + RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript + ARG MODEL_SIZE + RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" + FROM scratch + ARG MODEL_SIZE + COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | --------------------------------------- | --------------------------------- | + | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `rfdetr` | + yaml: |- + detectors: + ov: + type: openvino + device: GPU + + model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx # use the filename you generated above + - key: dfine + label: D-FINE / DEIMv2 + recommended: false + download: |- + #### D-FINE + + D-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. + + ```sh + docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /dfine + RUN git clone https://github.com/Peterande/D-FINE.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx onnxruntime onnxsim onnxscript + # Create output directory and download checkpoint + RUN mkdir -p output + ARG MODEL_SIZE + RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth + # Modify line 58 of export_onnx.py to change batch size to 1 + RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth + FROM scratch + ARG MODEL_SIZE + COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx + EOF + ``` + + #### DEIMv2 + + [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: + + - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n` + - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x` + + Set `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`). + + ```sh + docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF' + FROM python:3.11-slim AS build + RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /deimv2 + RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git . + # Install CPU-only PyTorch first to avoid pulling CUDA variant + RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu + RUN uv pip install --no-cache --system -r requirements.txt + RUN uv pip install --no-cache --system onnx safetensors huggingface_hub + RUN mkdir -p output + ARG BACKBONE + ARG MODEL_SIZE + # Download from Hugging Face and convert safetensors to pth + RUN python3 -c "\ + from huggingface_hub import hf_hub_download; \ + from safetensors.torch import load_file; \ + import torch; \ + backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \ + size = '${MODEL_SIZE}'.upper(); \ + st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \ + torch.save({'model': st}, 'output/deimv2.pth')" + RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py + # HuggingFace safetensors omits frozen constants that the model constructor initializes + RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth + FROM scratch + ARG BACKBONE + ARG MODEL_SIZE + COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | ---------------------------------------- | ---------------------------------- | + | **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `dfine` | + yaml: |- + detectors: + ov: + type: openvino + device: CPU + + model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine-s.onnx # use the filename you generated above + 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`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN 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 + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + apple-silicon: + type: zmq + endpoint: tcp://host.docker.internal:5555 + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + 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`). + + ```sh + docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /yolov9 + ADD https://github.com/WongKinYiu/yolov9.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript + ARG MODEL_SIZE + ARG IMG_SIZE + ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt + RUN 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 + RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx + FROM scratch + ARG MODEL_SIZE + ARG IMG_SIZE + COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + 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. + + ```sh + docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF' + FROM python:3.12 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ + WORKDIR /rfdetr + RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript + ARG MODEL_SIZE + RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" + FROM scratch + ARG MODEL_SIZE + COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | --------------------------------------- | --------------------------------- | + | **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` (use the filename you generated above) | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `rfdetr` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: rfdetr + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/rfdetr.onnx # use the filename you generated above + - 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). + + :::warning + + 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 + + ::: + + The 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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Custom object detector model path** | `/config/yolo_nas_s.onnx` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match whatever was set in notebook) | + | **Object detection model input height** | `320` (should match whatever was set in notebook) | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolonas + width: 320 # <--- should match whatever was set in notebook + height: 320 # <--- should match whatever was set in notebook + input_pixel_format: bgr + input_tensor: nchw + path: /config/yolo_nas_s.onnx + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `416` (should match the imgsize set during model export) | + | **Object detection model input height** | `416` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float_denorm` | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolox + width: 416 # <--- should match the imgsize set during model export + height: 416 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float_denorm + path: /config/model_cache/yolox_tiny.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: dfine + label: D-FINE / DEIMv2 + recommended: false + download: |- + #### Downloading D-FINE Model + + D-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. + + ```sh + docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' + FROM python:3.11 AS build + RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /dfine + RUN git clone https://github.com/Peterande/D-FINE.git . + RUN uv pip install --system -r requirements.txt + RUN uv pip install --system onnx onnxruntime onnxsim onnxscript + # Create output directory and download checkpoint + RUN mkdir -p output + ARG MODEL_SIZE + RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth + # Modify line 58 of export_onnx.py to change batch size to 1 + RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth + FROM scratch + ARG MODEL_SIZE + COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx + EOF + ``` + + #### Downloading DEIMv2 Model + + [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: + + - **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n` + - **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x` + + Set `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`). + + ```sh + docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF' + FROM python:3.11-slim AS build + RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* + COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ + WORKDIR /deimv2 + RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git . + # Install CPU-only PyTorch first to avoid pulling CUDA variant + RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu + RUN uv pip install --no-cache --system -r requirements.txt + RUN uv pip install --no-cache --system onnx safetensors huggingface_hub + RUN mkdir -p output + ARG BACKBONE + ARG MODEL_SIZE + # Download from Hugging Face and convert safetensors to pth + RUN python3 -c "\ + from huggingface_hub import hf_hub_download; \ + from safetensors.torch import load_file; \ + import torch; \ + backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \ + size = '${MODEL_SIZE}'.upper(); \ + st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \ + torch.save({'model': st}, 'output/deimv2.pth')" + RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py + # HuggingFace safetensors omits frozen constants that the model constructor initializes + RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py + RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth + FROM scratch + ARG BACKBONE + ARG MODEL_SIZE + COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx + EOF + ``` + 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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `dfine` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: dfine + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float + path: /config/model_cache/dfine_m_obj2coco.onnx # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + - key: yolo-legacy + label: YOLO (v3, v4, v7) + recommended: false + download: |- + To export as ONNX: + + ```sh + git clone https://github.com/NateMeyer/tensorrt_demos + cd tensorrt_demos/yolo + ./download_yolo.sh + python3 yolo_to_onnx.py -m yolov7-320 + ``` + 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------------- | + | **Custom object detector model path** | `/config/model_cache/yolo.onnx` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (should match the imgsize set during model export) | + | **Object detection model input height** | `320` (should match the imgsize set during model export) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + onnx: + type: onnx + + model: + model_type: yolo-generic + width: 320 # <--- should match the imgsize set during model export + height: 320 # <--- should match the imgsize set during model export + input_tensor: nchw + input_dtype: float + path: /config/model_cache/yolo.onnx # use the filename you generated above + 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). + + | Field | Value | + | ----------------- | ----- | + | **Detector type** | `cpu` | + | **Num threads** | `3` | + yaml: |- + detectors: + cpu1: + type: cpu + 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://:/v1/vision/detection`). + + | Field | Value | + | ------------- | ---------------------------------------------------------------------- | + | **API URL** | `http://:/v1/vision/detection` | + | **API Timeout** | `0.1` (seconds) | + yaml: |- + detectors: + deepstack: + api_url: http://:/v1/vision/detection + type: deepstack + 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). + + **Note:** The default model for the MemryX detector is YOLO-NAS 320x320. + + The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. + + - The default size of **320x320** is optimized for lower CPU usage and faster inference times. + + MemryX `.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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (can be set to `640` for higher resolution) | + | **Object detection model input height** | `320` (can be set to `640` for higher resolution) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolonas + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolonas.zip + # The .zip file must contain: + # ├── yolonas.dfp (a file ending with .dfp) + # └── 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). + + MemryX `.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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (can be set to `640` for higher resolution) | + | **Object detection model input height** | `320` (can be set to `640` for higher resolution) | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolo-generic + width: 320 # (Can be set to 640 for higher resolution) + height: 320 # (Can be set to 640 for higher resolution) + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolov9.zip + # The .zip file must contain: + # ├── 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. + + MemryX `.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: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `640` | + | **Object detection model input height** | `640` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float_denorm` | + | **Object Detection Model Type** | `yolox` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: yolox + width: 640 + height: 640 + input_tensor: nchw + input_dtype: float_denorm + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/yolox.zip + # The .zip file must contain: + # ├── 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. + + MemryX `.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: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `float` | + | **Object Detection Model Type** | `ssd` | + yaml: |- + detectors: + memx0: + type: memryx + device: PCIe:0 + + model: + model_type: ssd + width: 320 + height: 320 + input_tensor: nchw + input_dtype: float + labelmap_path: /labelmap/coco-80.txt + # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. + # path: /config/ssdlite_mobilenet.zip + # The .zip file must contain: + # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) + # └── 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. + + By 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. + + An example `docker-compose.yml` fragment that converts the `yolov7-320` and `yolov7x-640` models: + + ```yml + frigate: + environment: + - YOLO_MODELS=yolov7-320,yolov7x-640 + - USE_FP16=false + ``` + 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: + + | Field | Value | + | ---------------------------------------- | ------------------------------------------------------------ | + | **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` (use the filename you generated above) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | + | **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | + | **Model Input Pixel Color Format** | `rgb` | + | **Model Input Tensor Shape** | `nchw` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: + tensorrt: + type: tensorrt + device: 0 #This is the default, select the first GPU + + model: + path: /config/model_cache/tensorrt/yolov7-320.trt # use the filename you generated above + labelmap_path: /labelmap/coco-80.txt + input_tensor: nchw + input_pixel_format: rgb + width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416 + 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: + + | Field | Value | + | ---------------------------------------- | ---------------------------- | + | **Custom object detector model path** | `/synaptics/mobilenet.synap` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `224` | + | **Object detection model input height** | `224` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `ssd` (Frigate's default value) | + yaml: |- + detectors: # required + synap_npu: # required + type: synaptics # required + + model: # required + path: /synaptics/mobilenet.synap # required + width: 224 # required + height: 224 # required + input_tensor: nhwc # default value (optional. If you change the model, it is required) + 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. + + You 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: + + | Field | Value | + | ---------------------------------------- | -------------------------------------------------- | + | **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - frigate-fp16-yolov9-t + # - frigate-fp16-yolov9-s + # - frigate-fp16-yolov9-m + # - frigate-fp16-yolov9-c + # - frigate-fp16-yolov9-e + # your yolo_model.rknn + path: frigate-fp16-yolov9-t + model_type: yolo-generic + width: 320 + height: 320 + input_tensor: nhwc + 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. + + You 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. + + **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: + + | Field | Value | + | ---------------------------------------- | ----------------------------------------------------------------------- | + | **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolonas` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - deci-fp16-yolonas_s + # - deci-fp16-yolonas_m + # - deci-fp16-yolonas_l + # your yolonas_model.rknn + path: deci-fp16-yolonas_s + model_type: yolonas + width: 320 + height: 320 + input_pixel_format: bgr + input_tensor: nhwc + 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. + + You 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: + + | Field | Value | + | ---------------------------------------- | ---------------------------------------------- | + | **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `416` | + | **Object detection model input height** | `416` | + | **Model Input Pixel Color Format** | `rgb` (Frigate's default value) | + | **Model Input Tensor Shape** | `nhwc` | + | **Model Input D Type** | `int` (Frigate's default value) | + | **Object Detection Model Type** | `yolox` | + yaml: |- + model: # required + # name of model (will be automatically downloaded) or path to your own .rknn model file + # possible values are: + # - rock-i8-yolox_nano + # - rock-i8-yolox_tiny + # - rock-fp16-yolox_nano + # - rock-fp16-yolox_tiny + # your yolox_model.rknn + path: rock-i8-yolox_nano + model_type: yolox + width: 416 + height: 416 + input_tensor: nhwc + 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: + + | Field | Value | + | ---------------------------------------- | ----------------------- | + | **Custom object detector model path** | `frigate-yolov9-tiny` | + | **Label map for custom object detector** | `/labelmap/coco-80.txt` | + | **Object detection model input width** | `320` | + | **Object detection model input height** | `320` | + | **Model Input Pixel Color Format** | `bgr` | + | **Model Input Tensor Shape** | `nhwc` (Frigate's default value) | + | **Model Input D Type** | `int` | + | **Object Detection Model Type** | `yolo-generic` | + yaml: |- + detectors: + axengine: + type: axengine + + model: + path: frigate-yolov9-tiny + model_type: yolo-generic + width: 320 + height: 320 + input_dtype: int + input_pixel_format: bgr + 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`: + + ```yaml + degirum_detector: + container_name: degirum + image: degirum/aiserver:latest + privileged: true + ports: + - "8778:8778" + ``` + + Set `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**. + + | Field | Value | + | --- | --- | + | **Location** | `degirum` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: degirum + zoo: degirum/public + token: dg_example_token +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**. + + | Field | Value | + | --- | --- | + | **Location** | `@local` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: @local + zoo: degirum/public + token: dg_example_token +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**. + + | Field | Value | + | --- | --- | + | **Location** | `@cloud` | + | **Zoo** | `degirum/public` | + | **Token** | your AI Hub token (optional for the public zoo) | + yaml: | + degirum_detector: + type: degirum + location: @cloud + zoo: degirum/public + token: dg_example_token diff --git a/docs/docs/configuration/advanced/reference.md b/docs/docs/configuration/advanced/reference.md index 5c07e98a96..896ab2642b 100644 --- a/docs/docs/configuration/advanced/reference.md +++ b/docs/docs/configuration/advanced/reference.md @@ -212,7 +212,6 @@ audio: listen: - bark - fire_alarm - - scream - speech - yell # Optional: Filters to configure detection. @@ -340,7 +339,7 @@ detect: # especially when using separate streams for detect and record. # Use this setting to make the timeline bounding boxes more closely align # with the recording. The value can be positive or negative. - # TIP: Imagine there is an tracked object clip with a person walking from left to right. + # TIP: Imagine there is a tracked object clip with a person walking from left to right. # If the tracked object lifecycle bounding box is consistently to the left of the person # then the value should be decreased. Similarly, if a person is walking from # left to right and the bounding box is consistently ahead of the person @@ -656,11 +655,6 @@ snapshots: retain: # Required: Default retention days (default: shown below) default: 10 - # Optional: Mode for retention. (default: shown below) - # all - save all snapshots regardless of activity - # motion - save snapshots for any detected motion - # active_objects - save snapshots for active/moving objects - mode: motion # Optional: Per object retention days objects: person: 15 @@ -826,7 +820,7 @@ classification: interval: None # Optional: Restream configuration -# Uses https://github.com/AlexxIT/go2rtc (v1.9.13) +# Uses https://github.com/AlexxIT/go2rtc (v1.9.14) # NOTE: The default go2rtc API port (1984) must be used, # changing this port for the integrated go2rtc instance is not supported. go2rtc: diff --git a/docs/docs/configuration/advanced/system.md b/docs/docs/configuration/advanced/system.md index 71ef01234c..28291efff7 100644 --- a/docs/docs/configuration/advanced/system.md +++ b/docs/docs/configuration/advanced/system.md @@ -67,15 +67,21 @@ This section can be used to set environment variables for those unable to modify Variables prefixed with `FRIGATE_` can be referenced in config fields that support environment variable substitution (such as MQTT host and credentials, camera stream URLs, and ONVIF host and credentials) using the `{FRIGATE_VARIABLE_NAME}` syntax. +:::note + +The `go2rtc` section is an exception. go2rtc runs as a separate process, so its stream definitions can only be substituted with variables that exist in the container's environment (set via Docker `-e`, the `environment:` section of `docker-compose.yml`, or Docker secrets). Variables defined in the `environment_vars` block above are not available to go2rtc streams. Home Assistant app users, who cannot set container environment variables, must instead put credentials directly in their go2rtc stream URLs. + +::: + Navigate to to add or edit environment variables. -| Field | Description | -| --------- | --------------------------------------------------------- | -| **Key** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | -| **Value** | The value for the variable | +| Field | Description | +| ----------------- | --------------------------------------------------------- | +| **Variable name** | The environment variable name (e.g., `FRIGATE_MQTT_USER`) | +| **Value** | The value for the variable | Variables defined here can be referenced elsewhere in your configuration using the `{FRIGATE_VARIABLE_NAME}` syntax. @@ -237,7 +243,7 @@ Frigate exposes a few networking options. IPv6 and the listen ports are set in t ### Enabling IPv6 -By default Frigate listens on IPv4 only. To also listen on IPv6 — on port `5000`, and on `8971` when TLS is configured — enable it in the `networking` configuration. +By default Frigate listens on IPv4 only. To also listen on IPv6 (on port `5000`, and on `8971` when TLS is configured), enable it in the `networking` configuration. @@ -354,7 +360,7 @@ To do this: ### Custom go2rtc version -Frigate currently includes go2rtc v1.9.13, there may be certain cases where you want to run a different version of go2rtc. +Frigate currently includes go2rtc v1.9.14, there may be certain cases where you want to run a different version of go2rtc. To do this: diff --git a/docs/docs/configuration/audio_detectors.md b/docs/docs/configuration/audio_detectors.md index eba22ec184..03c68895bd 100644 --- a/docs/docs/configuration/audio_detectors.md +++ b/docs/docs/configuration/audio_detectors.md @@ -54,7 +54,7 @@ The ffmpeg process for capturing audio will be a separate connection to the came -Navigate to and add an input with the `audio` role pointing to a stream that includes audio. +Navigate to and add an input with the `audio` role pointing to a stream that includes audio. @@ -78,7 +78,7 @@ cameras: ### Configuring Minimum Volume -The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The Debug view in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are is displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. +The audio detector uses volume levels in the same way that motion in a camera feed is used for object detection. This means that Frigate will not run audio detection unless the audio volume is above the configured level in order to reduce resource usage. Audio levels can vary widely between camera models so it is important to run tests to see what volume levels are. The [Debug view](/usage/live#the-single-camera-view) in the Frigate UI has an Audio tab for cameras that have the `audio` role assigned where a graph and the current levels are displayed. The `min_volume` parameter should be set to the minimum the `RMS` level required to run audio detection. :::tip @@ -88,7 +88,7 @@ Volume is considered motion for recordings, this means when the `record -> retai ### Configuring Audio Events -The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `scream`, `speech`, and `yell` are enabled but these can be customized. +The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `speech`, and `yell` are enabled but these can be customized. @@ -107,7 +107,6 @@ audio: listen: - bark - fire_alarm - - scream - speech - yell ``` @@ -115,9 +114,73 @@ audio: +### Common Audio Labels + +The labelmap includes hundreds of sound types. The labels below are the ones most users may find practical, grouped by what they're typically used for. Use the exact label string from the left column in your `listen` config, or search for the label in the Frigate UI directly. + +Some labels cover several related sounds: `yell` is triggered by shouting, yelling, children shouting, and screaming; `crying` covers baby cries, sobbing, and whimpering; and `speech` covers ordinary talking and conversation. + +**Safety and security** + +| Label | Detects | +| ---------------- | ---------------------------------- | +| `yell` | Shouting, yelling, screaming | +| `fire_alarm` | Fire and smoke alarm sirens | +| `smoke_detector` | Smoke detector beeps | +| `alarm` | General alarm sounds | +| `car_alarm` | Car alarms | +| `siren` | Emergency vehicle and civil sirens | +| `glass` | Glass clinking | +| `shatter` | Breaking glass | +| `breaking` | Something breaking | +| `gunshot` | Gunshots | +| `explosion` | Explosions | + +**People and activity** + +| Label | Detects | +| ----------- | ------------------------ | +| `speech` | Talking and conversation | +| `laughter` | Laughing | +| `crying` | Baby crying and sobbing | +| `cough` | Coughing | +| `footsteps` | Footsteps and walking | +| `knock` | Knocking on a door | +| `doorbell` | Doorbell | +| `ding-dong` | Doorbell chime | + +**Pets and animals** + +| Label | Detects | +| ---------- | ---------------- | +| `bark` | Dog barking | +| `dog` | Other dog sounds | +| `howl` | Howling | +| `growling` | Growling | +| `meow` | Cat meowing | +| `cat` | Other cat sounds | +| `hiss` | Hissing | + +**Vehicles and driveway** + +| Label | Detects | +| ----------------- | -------------------- | +| `car` | Passing cars | +| `honk` | Car horns | +| `truck` | Trucks | +| `reversing_beeps` | Vehicle backup beeps | +| `motorcycle` | Motorcycles | +| `engine_starting` | Engines starting | + +:::tip + +Frequently-heard labels like `speech` can generate a lot of events, and each event could save a snapshot and recording based on your configuration, so start with a focused set and expand from there. The defaults (`bark`, `fire_alarm`, `speech`, `yell`) plus a few of the safety labels above cover most needs. See the [full audio labelmap](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) or the Frigate UI for every available type. + +::: + ### Audio Transcription -Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background. +Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service. Automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background. :::info diff --git a/docs/docs/configuration/authentication.md b/docs/docs/configuration/authentication.md index 0d80d80ce2..a565398774 100644 --- a/docs/docs/configuration/authentication.md +++ b/docs/docs/configuration/authentication.md @@ -91,7 +91,7 @@ auth: ## Session Length -The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required — otherwise, the user will need to log in again. +The default session length for user authentication in Frigate is 24 hours. This setting determines how long a user's authenticated session remains active before a token refresh is required. Otherwise, the user will need to log in again. While the default provides a balance of security and convenience, you can customize this duration to suit your specific security requirements and user experience preferences. The session length is configured in seconds. @@ -141,7 +141,7 @@ Changing the secret will invalidate current tokens. ## Proxy configuration -Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. +Frigate can be configured to leverage features of common upstream authentication proxies such as Authelia, Authentik, oauth2_proxy, or traefik-forward-auth. Frigate does not implement OIDC, SAML, or LDAP natively; as an NVR focused on recording and object detection, it relies on robust, battle-tested proxies to handle those protocols and passes the authenticated user and role through via headers (see below). If you are leveraging the authentication of an upstream proxy, you likely want to disable Frigate's authentication as there is no correspondence between users in Frigate's database and users authenticated via the proxy. Optionally, if communication between the reverse proxy and Frigate is over an untrusted network, you should set an `auth_secret` in the `proxy` config and configure the proxy to send the secret value as a header named `X-Proxy-Secret`. Assuming this is an untrusted network, you will also want to [configure a real TLS certificate](tls.md) to ensure the traffic can't simply be sniffed to steal the secret. diff --git a/docs/docs/configuration/autotracking.md b/docs/docs/configuration/autotracking.md index 7d0f8359d4..f49026e11c 100644 --- a/docs/docs/configuration/autotracking.md +++ b/docs/docs/configuration/autotracking.md @@ -6,6 +6,7 @@ title: Camera Autotracking import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; An ONVIF-capable, PTZ (pan-tilt-zoom) camera that supports relative movement within the field of view (FOV) can be configured to automatically track moving objects and keep them in the center of the frame. @@ -161,7 +162,7 @@ Every PTZ camera is different, so autotracking may not perform ideally in every The object tracker in Frigate estimates the motion of the PTZ so that tracked objects are preserved when the camera moves. In most cases 5 fps is sufficient, but if you plan to track faster moving objects, you may want to increase this slightly. Higher frame rates (> 10fps) will only slow down Frigate and the motion estimator and may lead to dropped frames, especially if you are using experimental zooming. -A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's debug viewer for your camera to see a thicker colored box around the object currently being autotracked. +A fast [detector](object_detectors.md) is recommended. CPU detectors will not perform well or won't work at all. You can watch Frigate's [debug viewer](/usage/live#the-single-camera-view) for your camera to see a thicker colored box around the object currently being autotracked. ![Autotracking Debug View](/img/autotracking-debug.gif) @@ -187,30 +188,96 @@ In security and surveillance, it's common to use "spotter" cameras in combinatio ## Troubleshooting and FAQ -### The autotracker loses track of my object. Why? +### Camera Compatibility + + + +See the community-maintained list of [ONVIF PTZ camera recommendations](cameras.md#onvif-ptz-camera-recommendations) for cameras and brands reported to work (and not work) with autotracking. This is not an exhaustive list that is frequently updated, so other cameras not listed may also work well. Frigate's autotracking was developed with a Dahua SD1A404XB-GNR (now sold as the EmpireTech PTZ1A4M-4X-S2), and Dahua / EmpireTech PTZs are the most consistently reported as working well. + +When comparing models: + +- Verify ONVIF support first. See [Checking ONVIF camera support](#checking-onvif-camera-support) above. +- Favor a camera with a fast PTZ motor. Cameras with slow motors may fail [calibration](#calibration) and will struggle to keep up with objects that move across the field of view quickly. + + + + + +No. Reolink cameras (including the TrackMix series) lack the ONVIF FOV RelativeMove firmware support that Frigate's autotracker requires, so autotracking will not work with any current Reolink PTZ. Their video streams and basic PTZ controls still work in Frigate. If you want object tracking on a Reolink PTZ, you will need to use the tracking feature built into the camera's firmware, which is proprietary and operates independently of Frigate. + + + + + +There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware, such as some Annke models. In rare cases the vendor may provide fixed firmware on request; for example, Annke has supplied firmware that resolves this for the CZ504 (see the [camera recommendations list](cameras.md#onvif-ptz-camera-recommendations)). + + + + + +Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever. + + + +### Calibration Issues + + + +This is often caused by the same reason as the "MOVING" status error above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking. + + + + + +Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner. + +Some things to try: + +- If your camera's firmware has a PTZ or motor speed setting, set it to the fastest available speed and calibrate again. +- Run without calibration: remove the `movement_weights` line from your config, set `calibrate_on_startup` to `False`, and restart. + +If calibration consistently fails, this often means your camera's motor is too slow and autotracking will behave unpredictably or won't be able to keep up with moving objects. + + + + + +Frigate uses the `movement_weights` measured during calibration to predict how far the camera needs to move to keep an object centered, so inaccurate values produce movements that don't seem to make sense: overshooting, moving the opposite direction, or zooming in on an object's last known position and losing it entirely. This is almost always a calibration issue. + +- Remove the `movement_weights` entry from your config and restart Frigate to run without calibration. If tracking improves, try recalibrating. +- Recalibrate several times. The `movement_weights` values should be close to each other after each run. If they vary significantly between runs, your camera may not be reporting its motor status reliably, and you may get better results without calibration. +- If you are using zooming, a high `zoom_factor` can cause the camera to zoom in too far and lose the object. Try a lower value. + +Remember to recalibrate whenever you change your `return_preset`, change your camera's detect `fps`, or enable zooming after calibrating with it disabled. + + + +### Tracking Behavior + + There are many reasons this could be the case. If you are using experimental zooming, your `zoom_factor` value might be too high, the object might be traveling too quickly, the scene might be too dark, there are not enough details in the scene (for example, a PTZ looking down on a driveway or other monotone background without a sufficient number of hard edges or corners), or the scene is otherwise less than optimal for Frigate to maintain tracking. Your camera's shutter speed may also be set too low so that blurring occurs with motion. Check your camera's firmware to see if you can increase the shutter speed. -Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it. +Watching Frigate's debug view can help to determine a possible cause. The autotracked object will have a thicker colored box around it. If the camera consistently zooms in on the object and then loses it, see [Autotracking is erratic, moves the camera in the wrong direction, or zooms past my object. Why?](#autotracking-is-erratic-or-moves-the-camera-in-the-wrong-direction) above. -### I'm seeing an error in the logs that my camera "is still in ONVIF 'MOVING' status." What does this mean? + -There are two possible known reasons for this (and perhaps others yet unknown): a slow PTZ motor or buggy camera firmware. Frigate uses an ONVIF parameter provided by the camera, `MoveStatus`, to determine when the PTZ's motor is moving or idle. According to some users, Hikvision PTZs (even with the latest firmware), are not updating this value after PTZ movement. Unfortunately there is no workaround to this bug in Hikvision firmware, so autotracking will not function correctly and should be disabled in your config. This may also be the case with other non-Hikvision cameras utilizing Hikvision firmware. - -### I tried calibrating my camera, but the logs show that it is stuck at 0% and Frigate is not starting up. - -This is often caused by the same reason as above - the `MoveStatus` ONVIF parameter is not changing due to a bug in your camera's firmware. Also, see the note above: Frigate's web UI and all other cameras will be unresponsive while calibration is in progress. This is expected and normal. But if you don't see log entries every few seconds for calibration progress, your camera is not compatible with autotracking. - -### I'm seeing this error in the logs: "Autotracker: motion estimator couldn't get transformations". What does this mean? + To maintain object tracking during PTZ moves, Frigate tracks the motion of your camera based on the details of the frame. If you are seeing this message, it could mean that your `zoom_factor` may be set too high, the scene around your detected object does not have enough details (like hard edges or color variations), or your camera's shutter speed is too slow and motion blur is occurring. Try reducing `zoom_factor`, finding a way to alter the scene around your object, or changing your camera's shutter speed. -### Calibration seems to have completed, but the camera is not actually moving to track my object. Why? + -Some cameras have firmware that reports that FOV RelativeMove, the ONVIF command that Frigate uses for autotracking, is supported. However, if the camera does not pan or tilt when an object comes into the required zone, your camera's firmware does not actually support FOV RelativeMove. One such camera is the Uniview IPC672LR-AX4DUPK. It actually moves its zoom motor instead of panning and tilting and does not follow the ONVIF standard whatsoever. + -### Frigate reports an error saying that calibration has failed. Why? +When the PTZ moves, the entire frame changes at once. Frigate's motion detection treats sudden scene-wide changes (like a lightning flash, an infrared mode switch, or a camera move) specially and pauses detection momentarily until the scene stabilizes. This is expected and normal, and detection resumes shortly after the camera stops moving. If detection does not resume once the camera is stationary, use the [debug view](/usage/live#the-single-camera-view) to see what is happening. -Calibration measures the amount of time it takes for Frigate to make a series of movements with your PTZ. This error message is recorded in the log if these values are too high for Frigate to support calibrated autotracking. This is often the case when your camera's motor or network connection is too slow or your camera's firmware doesn't report the motor status in a timely manner. You can try running without calibration (just remove the `movement_weights` line from your config and restart), but if calibration fails, this often means that autotracking will behave unpredictably. + + + + +Yes. Autotracking can be toggled per camera at runtime over MQTT with the [`frigate//ptz_autotracker/set`](../integrations/mqtt.md#frigatecamera_nameptz_autotrackerset) topic, and the [Home Assistant integration](../integrations/home-assistant.md) exposes a switch for it. This pairs well with the "spotter" camera automations described in [Usage applications](#usage-applications) above, for example only enabling autotracking at night or when nobody is home. + + diff --git a/docs/docs/configuration/birdseye.md b/docs/docs/configuration/birdseye.md index 8104494787..26a2386d89 100644 --- a/docs/docs/configuration/birdseye.md +++ b/docs/docs/configuration/birdseye.md @@ -6,10 +6,16 @@ import NavPath from "@site/src/components/NavPath"; In addition to Frigate's Live camera dashboard, Birdseye allows a portable heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about. -Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the "+" icon on the Live page, and choose "Birdseye" as one of the cameras. +Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the pencil icon in the sidebar on the Live page, and choose "Birdseye" as one of the cameras. Birdseye can also be used in Home Assistant dashboards, cast to media devices, etc. +:::note + +Each camera tile in Birdseye is composed from the frames of the stream assigned the `detect` role, so a camera's image quality in Birdseye matches its detect stream resolution rather than a higher-resolution recording stream. If a camera looks low quality in Birdseye, increasing the detect width and height (or assigning the `detect` role to a higher-resolution stream) is what affects it. See [setting up camera inputs](./cameras.md#setting-up-camera-inputs) for how roles are assigned. + +::: + ## Birdseye Behavior ### Birdseye Modes @@ -35,10 +41,10 @@ To include a camera in Birdseye view only for specific circumstances, or exclude **Per-camera overrides:** Navigate to to override the mode or disable Birdseye for a specific camera. -| Field | Description | -|-------|-------------| -| **Enable Birdseye** | Whether this camera appears in Birdseye view | -| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` | +| Field | Description | +| ------------------- | ------------------------------------------------------------- | +| **Enable Birdseye** | Whether this camera appears in Birdseye view | +| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` | @@ -72,8 +78,8 @@ By default birdseye shows all cameras that have had the configured activity in t Navigate to . -| Field | Description | -|-------|-------------| +| Field | Description | +| ------------------------ | --------------------------------------------------------------------------- | | **Inactivity threshold** | Seconds of inactivity before a camera is hidden from Birdseye (default: 30) | @@ -100,9 +106,9 @@ The resolution and aspect ratio of birdseye can be configured. Resolution will i Navigate to . -| Field | Description | -|-------|-------------| -| **Width** | Birdseye output width in pixels (default: 1280) | +| Field | Description | +| ---------- | ----------------------------------------------- | +| **Width** | Birdseye output width in pixels (default: 1280) | | **Height** | Birdseye output height in pixels (default: 720) | @@ -120,12 +126,12 @@ birdseye: ### Sorting cameras in the Birdseye view -It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level. +It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level (when using YAML). -Navigate to for each camera and set the **Position** field to control the display order. +Navigate to and in the **Camera order** field, use the drag handle next to each camera name to control the display order. @@ -161,8 +167,8 @@ It is possible to limit the number of cameras shown on birdseye at one time. Whe Navigate to . -| Field | Description | -|-------|-------------| +| Field | Description | +| ------------------------ | ----------------------------------------------------------------------------------- | | **Layout > Max cameras** | Maximum number of cameras shown at once (e.g., `1` for only the most active camera) | @@ -187,8 +193,8 @@ By default birdseye tries to fit 2 cameras in each row and then double in size u Navigate to . -| Field | Description | -|-------|-------------| +| Field | Description | +| --------------------------- | -------------------------------------------------------- | | **Layout > Scaling factor** | Camera scaling factor between 1.0 and 5.0 (default: 2.0) | diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index 9d39ef2519..27eb729bfa 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -3,6 +3,8 @@ id: camera_specific title: Camera Specific Configurations --- +import NavPath from "@site/src/components/NavPath"; + :::note This page makes use of presets of FFmpeg args. For more information on presets, see the [FFmpeg Presets](/configuration/ffmpeg_presets) page. @@ -148,19 +150,34 @@ WEB Digest Algorithm - MD5 Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations. -| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | -| ----------------- | ------------------------- | --------------------------------- | ----------------------------------------------------------------------- | -| 5MP or lower | All | http-flv | Stream is h264 | -| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 | -| 6MP or higher | Older (ex: RLC-8##) | rtsp | | +| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | +| ----------------- | ------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | +| 5MP or lower | All | http-flv | Stream is h264 | +| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 (Frigate's default) | +| 6MP or higher | Older (ex: RLC-8##) | rtsp | | -Frigate works much better with newer reolink cameras that are setup with the below options: +Frigate works much better with newer Reolink cameras that are setup with the below options: If available, recommended settings are: - `On, fluency first` this sets the camera to CBR (constant bit rate) - `Interframe Space 1x` this sets the iframe interval to the same as the frame rate +#### Setup via the Add Camera Wizard + +The Add Camera Wizard is the recommended way to add a standard Reolink camera. Before starting, make sure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. The wizard uses the camera's HTTP API to determine its resolution and choose the recommended stream type from the table above. + +1. Click **Add Camera** in . +2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand. +3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower, or an RTSP stream for higher resolution cameras. +4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it. + +If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras where http-flv is recommended, the wizard will show a warning in the validation step. + +The wizard covers standard single-camera setups. For two way talk, cameras connected through a Reolink NVR, or audio transcoding for WebRTC live view, configure the camera manually as shown below. + +#### Manual configuration + According to [this discussion](https://github.com/blakeblackshear/frigate/issues/3235#issuecomment-1135876973), the http video streams seem to be the most reliable for Reolink. Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels. @@ -175,7 +192,7 @@ Reolink's latest cameras support two way audio via go2rtc and other applications NOTE: The RTSP stream can not be prefixed with `ffmpeg:`, as go2rtc needs to handle the stream to support two way audio. -Ensure HTTP is enabled in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). +Ensure [HTTP is enabled](https://support.reolink.com/articles/360003452893-How-to-Access-Reolink-Cameras-NVRs-Home-Hub-Locally-via-Web-Browsers/) in the camera's advanced network settings. To use two way talk with Frigate, see the [Live view documentation](/configuration/live#two-way-talk). ::: @@ -187,7 +204,7 @@ go2rtc: - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" your_reolink_camera_sub: - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password" - # example for connectin to a Reolink camera that supports two way talk + # example for connecting to a Reolink camera that supports two way talk your_reolink_camera_twt: - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus" - "rtsp://username:password@reolink_ip/Preview_01_sub" @@ -225,13 +242,14 @@ cameras: roles: - detect ``` + ### Unifi Protect Cameras -:::note +:::note -Unifi G5s cameras and newer need a Unifi Protect server to enable rtsps stream, it's not posible to enable it in standalone mode. +Unifi G5s cameras and newer need a Unifi Protect server to enable rtsps stream, it's not possible to enable it in standalone mode. ::: @@ -246,7 +264,7 @@ go2rtc: - rtspx://192.168.1.1:7441/abcdefghijk ``` -[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-rtsp) +[See the go2rtc docs for more information](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-rtsp) In the Unifi 2.0 update Unifi Protect Cameras had a change in audio sample rate which causes issues for ffmpeg. The input rate needs to be set for record if used directly with unifi protect. @@ -269,7 +287,6 @@ Some community members have found better performance on Wyze cameras by using an To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's [FFmpeg Device](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg-device) support: - Preparation outside of Frigate: - - Get USB camera path. Run `v4l2-ctl --list-devices` to get a listing of locally-connected cameras available. (You may need to install `v4l-utils` in a way appropriate for your Linux distribution). In the sample configuration below, we use `video=0` to correlate with a detected device path of `/dev/video0` - Get USB camera formats & resolutions. Run `ffmpeg -f v4l2 -list_formats all -i /dev/video0` to get an idea of what formats and resolutions the USB Camera supports. In the sample configuration below, we use a width of 1024 and height of 576 in the stream and detection settings based on what was reported back. - If using Frigate in a container (e.g. Docker on TrueNAS), ensure you have USB Passthrough support enabled, along with a specific Host Device (`/dev/video0`) + Container Device (`/dev/video0`) listed. diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index e711c8ad56..dc93f30ce1 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -24,12 +24,14 @@ Each role can only be assigned to one input per camera. The options for roles ar -Navigate to . +Navigate to . | Field | Description | | ----------------- | ------------------------------------------------------------------- | | **Camera inputs** | List of input stream definitions (paths and roles) for this camera. | +For each input you can choose its source: select **Restream (go2rtc)** to pick an existing [go2rtc stream](restream.md) from a dropdown (Frigate uses the `rtsp://127.0.0.1:8554/` path and `preset-rtsp-restream` input args for that input automatically), or **Manual input path** to type the stream URL directly. + Navigate to . | Field | Description | @@ -192,7 +194,7 @@ Camera groups let you organize cameras together with a shared name and icon, mak -On the Live dashboard, press the **+** icon in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order. +On the Live dashboard, press the **pencil icon** in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order. diff --git a/docs/docs/configuration/config.md b/docs/docs/configuration/config.md index b2572efda7..edb6d0cd74 100644 --- a/docs/docs/configuration/config.md +++ b/docs/docs/configuration/config.md @@ -7,15 +7,58 @@ import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; -Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach — it provides validation and a guided experience for all configuration options. +Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach. It provides validation and a guided experience for all configuration options. -It is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md). +## Using the Settings UI + +The Settings UI groups every configuration option into sections that are listed in the left-hand menu. Each section presents a guided form with validation, so you don't need to remember the structure of the YAML or look up option names by hand. + +### Global vs. camera-level configuration + +Settings are organized into two scopes: + +- **Global configuration**: values under apply to every camera by default. This is where you set the baseline behavior for object detection, recording, snapshots, motion, and so on. +- **Camera configuration**: values under apply to a single camera. Use the camera selector button at the top of these pages to choose which camera you are editing. + +When a camera-level section is left untouched, the camera simply inherits the global values. Changing a value on a camera page **overrides** the global value for that camera only: the global setting and every other camera are unaffected. This mirrors how the YAML works, where a value set under `cameras.` takes precedence over the same value set at the top level. + +To undo an override and go back to inheriting from the parent scope, use the reset button at the bottom of the section: + +- On a camera section, the button is labeled **Reset to Global** and restores the camera to the global value. +- On a global section, the button is labeled **Reset to Default** and restores Frigate's built-in default. + +Resetting asks for confirmation and cannot be undone once applied. + +### Saving changes and the Save All button + +Edits are not applied until you save them. As soon as you change a value, the UI tracks it as a pending change: + +- The edited section shows a **Modified** badge, and the changed fields are highlighted. +- A **You have unsaved changes** notice appears above the section's **Save** and **Undo** buttons. **Save** commits just that section; **Undo** discards its pending edits. + +Because pending changes can span multiple sections (and multiple cameras), the header provides a **Save All** button that writes every pending change at once. Next to it, **Review pending changes** opens a summary that lists each pending edit with its scope (Global or a specific camera), the affected field, and the new value, so you can confirm exactly what will be written before committing. **Undo All** discards every pending change across all sections. + +### Restart-required indicators + +Most settings take effect immediately, but some require Frigate to restart before they apply. Fields that require a restart are marked with a small restart icon and a **Restart required** tooltip next to the field label. + +When you save a change that touches one of these fields, Frigate confirms the save and reminds you that a restart is needed (for example, _"Settings saved successfully. Restart Frigate to apply your changes."_). The notification includes a one-click **Restart Frigate** action so you can apply the change right away, or you can continue editing and restart later. + +### The colored dots in the camera configuration menu + +When you are working under , small colored dots can appear next to a section's name in the menu. They give you an at-a-glance summary of that section's state for the selected camera: + +- **Blue dot**: this section **overrides the global configuration**. One or more values in the section have been set specifically for this camera and differ from the global defaults. +- **Profile-colored dot**: when you are viewing a [camera profile](./profiles.md), a dot in that profile's assigned color indicates the section is **overridden by that profile**. Each profile is given its own distinct color so you can tell at a glance which sections it changes. +- **Amber dot**: this section has **unsaved changes**. It appears alongside the **Modified** badge whenever you have pending edits in the section that haven't been saved yet. + +Hover over any dot to see a tooltip describing what it means. Open a section to see exactly which fields are overridden: the section header indicates how many fields differ from the global (or base) configuration. ## Configuration File Location -For users who prefer to edit the YAML configuration file directly: +For users who prefer to edit the YAML configuration file directly, it is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md). -- **Home Assistant App:** `/addon_configs//config.yml` — see [directory list](#accessing-app-config-dir) +- **Home Assistant App:** `/addon_configs//config.yml` (see [directory list](#accessing-app-config-dir)) - **All other installations:** Map to `/config/config.yml` inside the container It can be named `config.yml` or `config.yaml`, but if both files exist `config.yml` will be preferred and `config.yaml` will be ignored. diff --git a/docs/docs/configuration/custom_classification/object_classification.md b/docs/docs/configuration/custom_classification/object_classification.md index 05b110bdaf..2bfe2c31cc 100644 --- a/docs/docs/configuration/custom_classification/object_classification.md +++ b/docs/docs/configuration/custom_classification/object_classification.md @@ -137,7 +137,7 @@ If examples for some of your classes do not appear in the grid, you can continue :::tip Diversity matters far more than volume -Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data — the model learns what _that exact moment_ looked like rather than what actually defines the class. **This is why Frigate does not implement bulk training in the UI.** +Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the class. **This is why Frigate does not implement bulk training in the UI.** For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374). @@ -155,7 +155,7 @@ For more detail, see [Frigate Tip: Best Practices for Training Face and Custom C :::tip `none` works differently from named classes -Named classes work best with visually uniform examples — every Buddy photo should look like Buddy. The `none` class needs the opposite: visual diversity across sizes, framings, and qualities, because at inference it has to absorb everything that isn't one of your named classes. Don't apply the same "only keep large, well-framed images" rule to `none` that you would to a named class. Mix in small crops, partial views, and false positives deliberately - otherwise the model has no signal for "small/ambiguous thing = not one of my known classes" and will force those crops into a named class by default. +Named classes work best with visually uniform examples. Every Buddy photo should look like Buddy. The `none` class needs the opposite: visual diversity across sizes, framings, and qualities, because at inference it has to absorb everything that isn't one of your named classes. Don't apply the same "only keep large, well-framed images" rule to `none` that you would to a named class. Mix in small crops, partial views, and false positives deliberately - otherwise the model has no signal for "small/ambiguous thing = not one of my known classes" and will force those crops into a named class by default. ::: diff --git a/docs/docs/configuration/custom_classification/state_classification.md b/docs/docs/configuration/custom_classification/state_classification.md index 8b32857d0e..7d479946bf 100644 --- a/docs/docs/configuration/custom_classification/state_classification.md +++ b/docs/docs/configuration/custom_classification/state_classification.md @@ -103,7 +103,7 @@ Once some images are assigned, training will begin automatically. :::tip Diversity matters far more than volume -Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data — the model learns what _that exact moment_ looked like rather than what actually defines the state. This often leads to models that work perfectly under the original conditions but become unstable when day turns to night, weather changes, or seasonal lighting shifts. **This is why Frigate does not implement bulk training in the UI.** +Selecting dozens of nearly identical images is one of the fastest ways to degrade model performance. MobileNetV2 can overfit quickly when trained on homogeneous data. The model learns what _that exact moment_ looked like rather than what actually defines the state. This often leads to models that work perfectly under the original conditions but become unstable when day turns to night, weather changes, or seasonal lighting shifts. **This is why Frigate does not implement bulk training in the UI.** For more detail, see [Frigate Tip: Best Practices for Training Face and Custom Classification Models](https://github.com/blakeblackshear/frigate/discussions/21374). diff --git a/docs/docs/configuration/face_recognition.md b/docs/docs/configuration/face_recognition.md index e4999c6e8f..96a29c0a9e 100644 --- a/docs/docs/configuration/face_recognition.md +++ b/docs/docs/configuration/face_recognition.md @@ -6,6 +6,7 @@ title: Face Recognition import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; Face recognition identifies known individuals by matching detected faces with previously learned facial data. When a known `person` is recognized, their name will be added as a `sub_label`. This information is included in the UI, filters, as well as in notifications. @@ -86,7 +87,7 @@ Navigate to . - **Detection threshold**: Face detection confidence score required before recognition runs. This field only applies to the standalone face detection model; `min_score` should be used to filter for models that have face detection built in. - Default: `0.7` - **Minimum face area**: Minimum size (in pixels) a face must be before recognition runs. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces. - - Default: `500` pixels + - Default: `750` pixels @@ -95,7 +96,7 @@ Navigate to . face_recognition: enabled: true detection_threshold: 0.7 - min_area: 500 + min_area: 750 ``` @@ -151,6 +152,14 @@ Follow these steps to begin: ## Creating a Robust Training Set +:::tip + +**The short version:** Start with a few clear, front-facing photos of each person. As faces are detected in the Recent Recognitions tab, train clear images that scored lower, adding variety (different angles, lighting, and expressions) slowly. Diversity matters far more than volume, and low-quality images hurt recognition more than they help. + +For a step-by-step narrative of these best practices (and the same principles applied to state and object classification), see the [Frigate Tips: Best Practices for Training](https://github.com/blakeblackshear/frigate/discussions/21374) discussion. + +::: + The number of images needed for a sufficient training set for face recognition varies depending on several factors: - Diversity of the dataset: A dataset with diverse images, including variations in lighting, pose, and facial expressions, will require fewer images per person than a less diverse dataset. @@ -181,9 +190,27 @@ When choosing images to include in the face training set it is recommended to al The Recent Recognitions tab in the face library displays recent face recognition attempts. Detected face images are grouped according to the person they were identified as potentially matching. -Each face image is labeled with a name (or `Unknown`) along with the confidence score of the recognition attempt. While each image can be used to train the system for a specific person, not all images are suitable for training. +Each face image is labeled with a name (or `Unknown`) along with the confidence score of that recognition attempt. Images are grouped by the person they were matched against, not by who they actually are, so a group labeled with a person's name can contain a crop that is really someone else but happened to score as a partial match. The name and score shown on each individual crop describe that single attempt. -Refer to the guidelines below for best practices on selecting images for training. +While each image can be used to train the system for a specific person, not all images are suitable for training. Refer to the guidelines below for best practices on selecting images for training. + +### How Frigate Decides Who a Person Is + +Recognition does not happen one frame at a time. While a `person` is in view, Frigate runs face recognition on many frames, not just a single frame. The final `sub_label` is decided from all of those attempts together, weighted by the area of each face (larger, closer faces count more), not from any single frame. + +This has a few practical consequences: + +- A handful of wrong guesses on blurry or distant frames usually do not change the result. If Frigate sees a person as "Tom, Tom, Sam, Tom, Tom," it will still conclude the person was Tom. +- The goal is not for every individual face crop to be correct. The goal is for each person to be recognized correctly overall, across all the faces captured while they were present. +- A single very high confidence match will not by itself assign a sub label. Recognition must be consistent. See [I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned?](#i-see-scores-above-the-threshold-in-the-recent-recognitions-tab-but-a-sub-label-wasnt-assigned) below. + +### Which Faces Are Worth Training? + +Whether a face is worth training has little to do with what it was recognized as. A crop is a good training candidate when all of these are true: + +- It did not already score high and correctly. Faces that are already recognized confidently add little and increase the risk of over-fitting. +- It is clear enough to be useful: not blurry, not heavily off-axis, not infrared (gray-scale). If it is hard for you to make out the face, it will not help the model. +- It adds something new: a different angle, lighting, expression, or distance than what you already have. ### Step 1 - Building a Strong Foundation @@ -199,14 +226,16 @@ Once front-facing images are performing well, start choosing slightly off-angle ## FAQ -### How do I debug Face Recognition issues? +### Getting Recognition Working + + Start with the [Usage](#usage) section and re-read the [Model Requirements](#model-requirements) above. 1. Ensure `person` is being _detected_. A `person` will automatically be scanned by Frigate for a face. Any detected faces will appear in the Recent Recognitions tab in the Frigate UI's Face Library. If you are using a Frigate+ or `face` detecting model: - - Watch the debug view (Settings --> Debug) to ensure that `face` is being detected along with `person`. + - Watch the [debug view](/usage/live#the-single-camera-view) to ensure that `face` is being detected along with `person`. - You may need to adjust the `min_score` for the `face` object if faces are not being detected. If you are **not** using a Frigate+ or `face` detecting model: @@ -217,21 +246,47 @@ Start with the [Usage](#usage) section and re-read the [Model Requirements](#mod - Make sure you have trained at least one face per the recommendations above. - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). -### Detection does not work well with blurry images? + -Accuracy is definitely a going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work. + + +Face recognition does not run on the recording stream, this would be suboptimal for many reasons: + +1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after. +2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway. +3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution. + + + +### Improving Accuracy and Training + + + +Accuracy is definitely going to be improved with higher quality cameras / streams. It is important to look at the DORI (Detection Observation Recognition Identification) range of your camera, if that specification is posted. This specification explains the distance from the camera that a person can be detected, observed, recognized, and identified. The identification range is the most relevant here, and the distance listed by the camera is the furthest that face recognition will realistically work. Some users have also noted that setting the stream in camera firmware to a constant bit rate (CBR) leads to better image clarity than with a variable bit rate (VBR). -### Why can't I bulk upload photos? + + + + +The embedding models are trained on color images, so gray-scale and infrared (IR) faces sit in a different feature distribution and are more easily confused with other people. Prefer color images, and avoid mixing gray-scale samples in early while you are building a foundation. If someone only ever appears at night, gray-scale training is acceptable, but keep those samples limited and as clear as possible, and add them only once color recognition is stable for your other people. + + + + It is important to methodically add photos to the library, bulk importing photos (especially from a general photo library) will lead to over-fitting in that particular scenario and hurt recognition performance. -### Why can't I bulk reprocess faces? + + + Face embedding models work by breaking apart faces into different features. This means that when reprocessing an image, only images from a similar angle will have its score affected. -### Why do unknown people score similarly to known people? + + + This can happen for a few different reasons, but this is usually an indicator that the training set needs to be improved. This is often related to over-fitting: @@ -241,33 +296,54 @@ This can happen for a few different reasons, but this is usually an indicator th Review your face collections and remove most of the unclear or low-quality images. Then, use the **Reprocess** button on each face in the **Train** tab to evaluate how the changes affect recognition scores. -Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower - ideally with different lighting, angles, and conditions—to help the model generalize more effectively. +Avoid training on images that already score highly, as this can lead to over-fitting. Instead, focus on relatively clear images that score lower (ideally with different lighting, angles, and conditions) to help the model generalize more effectively. -### Frigate misidentified a face. Can I tell it that a face is "not" a specific person? + + + + +Only if it is a good image. Reassigning a face does add it to that person's training set, but two things are true at once: + +- Reassigning a single misclassified frame has a small effect. The image is weighted against every other sample for that person, so correcting 1 frame out of 20 will not move recognition much. Occasional wrong guesses on poor frames are normal and do not need to be fixed. +- Reassigning a poor image (blurry, off-angle, low-resolution, gray-scale) can hurt more than the misidentification did, because low-quality samples degrade recognition for that whole person. + +So the decision is about image quality, not about the wrong label. If the crop is clear, well-lit, and reasonably front-facing, and it scored low or was wrong, assigning it to the correct person is useful. If you can barely make out the face yourself, ignore it; do not train it just to correct the label. + +If a person is repeatedly misidentified, do not keep reassigning the same frame. Instead, remove low-quality or misleading images and add a few high-quality samples to the correct person. See [Why do unknown people score similarly to known people?](#why-do-unknown-people-score-similarly-to-known-people) above. + + + + No, face recognition does not support negative training (i.e., explicitly telling it who someone is _not_). Instead, the best approach is to improve the training data by using a more diverse and representative set of images for each person. For more guidance, refer to the section above on improving recognition accuracy. -### I see scores above the threshold in the Recent Recognitions tab, but a sub label wasn't assigned? +This also applies to a stranger who is repeatedly matched to a known person (for example, a delivery driver recognized as you). Do not create a profile for them and do not reassign their faces to yourself, as this pollutes your training set and makes recognition worse. Leave the detection as unknown and improve the known person's training set instead. Face recognition learns who someone is, not who they are not. -The Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results. + -### Can I use other face recognition software like DoubleTake at the same time as the built in face recognition? + + +Frigate considers the recognition scores across all recognition attempts for each person object. The scores are continually weighted based on the area of the face, and a sub label will only be assigned to person if a person is confidently recognized consistently. This avoids cases where a single high confidence recognition would throw off the results. + + + +### Compatibility and Maintenance + + No, using another face recognition service will interfere with Frigate's built in face recognition. When using double-take the sub_label feature must be disabled if the built in face recognition is also desired. -### Does face recognition run on the recording stream? + -Face recognition does not run on the recording stream, this would be suboptimal for many reasons: - -1. The latency of accessing the recordings means the notifications would not include the names of recognized people because recognition would not complete until after. -2. The embedding models used run on a set image size, so larger images will be scaled down to match this anyway. -3. Motion clarity is much more important than extra pixels, over-compression and motion blur are much more detrimental to results than resolution. - -### I get an unknown error when taking a photo directly with my iPhone + By default iOS devices will use HEIC (High Efficiency Image Container) for images, but this format is not supported for uploads. Choosing `large` as the format instead of `original` will use JPG which will work correctly. -### How can I delete the face database and start over? + + + Frigate does not store anything in its database related to face recognition. You can simply delete all of your faces through the Frigate UI or remove the contents of the `/media/frigate/clips/faces` directory. + + diff --git a/docs/docs/configuration/ffmpeg_presets.md b/docs/docs/configuration/ffmpeg_presets.md index 3333882801..5c1d0fc3b1 100644 --- a/docs/docs/configuration/ffmpeg_presets.md +++ b/docs/docs/configuration/ffmpeg_presets.md @@ -7,33 +7,33 @@ import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; -Some presets of FFmpeg args are provided by default to make the configuration easier. All presets can be seen in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py). +Frigate ships with a set of FFmpeg presets to keep your configuration short and readable. Each preset expands to a longer list of FFmpeg arguments at runtime. You can see exactly what every preset expands to in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py). -### Hwaccel Presets +In the config file you reference a preset by its name (for example, `preset-vaapi`). In the UI, the same preset is shown with a friendly label (for example, **VAAPI (Intel/AMD GPU)**). Both refer to the same thing: the tables below list the config name alongside the label you'll see in the UI. -It is highly recommended to use hwaccel presets in the config. These presets not only replace the longer args, but they also give Frigate hints of what hardware is available and allows Frigate to make other optimizations using the GPU such as when encoding the birdseye restream or when scaling a stream that has a size different than the native stream size. +### Hwaccel (Hardware Acceleration) Presets {#hwaccel-presets} -See [the hwaccel docs](/configuration/hardware_acceleration_video.md) for more info on how to setup hwaccel for your GPU / iGPU. +Hardware acceleration arguments tell FFmpeg to decode your camera's video stream on a GPU or integrated graphics chip instead of the CPU, which dramatically lowers CPU usage. Using a preset is highly recommended. Beyond replacing a long list of arguments, each preset also tells Frigate what hardware is available so it can offload additional work to the GPU, for example, encoding the Birdseye restream or scaling a stream whose resolution differs from the camera's native size. -| Preset | Usage | Other Notes | -| --------------------- | ------------------------------ | ----------------------------------------------------- | -| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | | -| preset-rpi-64-h265 | 64 bit Rpi with h265 stream | | -| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen | -| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead | -| preset-intel-qsv-h265 | Intel QSV with h265 stream | If issues occur recommend using vaapi preset instead | -| preset-nvidia | Nvidia GPU | | -| preset-jetson-h264 | Nvidia Jetson with h264 stream | | -| preset-jetson-h265 | Nvidia Jetson with h265 stream | | -| preset-rkmpp | Rockchip MPP | Use image with \*-rk suffix and privileged mode | +See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md) for details on setting up hardware acceleration for your GPU / iGPU, then select the preset that matches your hardware. -Select the appropriate hwaccel preset for your hardware. +| Preset (YAML config) | UI Label | Usage | Notes | +| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- | +| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | | +| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | | +| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected | +| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead | +| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead | +| preset-nvidia | NVIDIA GPU | NVIDIA GPU | | +| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | | +| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | | +| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode | 1. Navigate to and set **Hardware acceleration arguments** to the appropriate preset for your hardware. -2. To override for a specific camera, navigate to and set **Hardware acceleration arguments** for that camera. +2. To override for a specific camera, navigate to and set **Hardware acceleration arguments** for that camera. @@ -53,25 +53,25 @@ cameras: ### Input Args Presets -Input args presets help make the config more readable and handle use cases for different types of streams to ensure maximum compatibility. +Input arguments are passed to FFmpeg before your camera source and control how Frigate connects to and reads the stream: the transport protocol, timeouts, reconnection behavior, and how the stream is probed. The right input args ensure a reliable connection and maximum compatibility for each type of stream. -See [the camera specific docs](/configuration/camera_specific.md) for more info on non-standard cameras and recommendations for using them in Frigate. +See [the camera-specific docs](/configuration/camera_specific.md) for more on non-standard cameras and recommendations for using them in Frigate. -| Preset | Usage | Other Notes | -| -------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ | -| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead | -| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead | -| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp | -| preset-rtmp-generic | RTMP Stream | | -| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified | -| preset-rtsp-restream | RTSP Stream from restream | Use for rtsp restream as source for frigate | -| preset-rtsp-restream-low-latency | RTSP Stream from restream | Use for rtsp restream as source for frigate to lower latency, may cause issues with some cameras | -| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only | -| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris | +| Preset (config) | UI Label | Usage | Notes | +| -------------------------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------------------- | +| preset-http-jpeg-generic | HTTP JPEG (Generic) | HTTP live JPEG | Restreaming the live JPEG is recommended instead | +| preset-http-mjpeg-generic | HTTP MJPEG (Generic) | HTTP MJPEG stream | Restreaming the MJPEG stream is recommended instead | +| preset-http-reolink | HTTP - Reolink Cameras | Reolink HTTP-FLV stream | Only for Reolink HTTP, not when restreaming as RTSP | +| preset-rtmp-generic | RTMP (Generic) | RTMP stream | | +| preset-rtsp-generic | RTSP (Generic) | RTSP stream | The default when no input args are specified | +| preset-rtsp-restream | RTSP - Restream from go2rtc | RTSP stream from a restream | Use when a go2rtc restream is the source for Frigate | +| preset-rtsp-restream-low-latency | RTSP - Restream from go2rtc (Low Latency) | RTSP stream from a restream | Lowers latency for a go2rtc restream source; may cause issues with some cameras | +| preset-rtsp-udp | RTSP - UDP | RTSP stream over UDP | Use when the camera only supports UDP | +| preset-rtsp-blue-iris | RTSP - Blue Iris | Blue Iris RTSP stream | Use when consuming a stream from Blue Iris | :::warning -It is important to be mindful of input args when using restream because you can have a mix of protocols. `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, when using a reolink cam with the rtsp restream as a source for record the preset-http-reolink will cause a crash. In this case presets will need to be set at the stream level. See the example below. +Be mindful of input arguments when restreaming, because you can end up with a mix of protocols. The `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, using a Reolink camera with an RTSP restream as the recording source while `preset-http-reolink` is applied will cause a crash. In cases like this, set the preset at the stream level instead. See the example below. ::: @@ -96,13 +96,13 @@ cameras: ### Output Args Presets -Output args presets help make the config more readable and handle use cases for different types of streams to ensure consistent recordings. +Output arguments are passed to FFmpeg after your camera source and control how recordings are written: which codecs are used and whether audio and video are copied as-is or re-encoded. The right output args ensure consistent, playable recordings for each type of stream. -| Preset | Usage | Other Notes | -| -------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| preset-record-generic | Record WITHOUT audio | If your camera doesn't have audio, or if you don't want to record audio, use this option | -| preset-record-generic-audio-copy | Record WITH original audio | Use this to enable audio in recordings | -| preset-record-generic-audio-aac | Record WITH transcoded aac audio | This is the default when no option is specified. Use it to transcode audio to AAC. If the source is already in AAC format, use preset-record-generic-audio-copy instead to avoid unnecessary re-encoding | -| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead | -| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead | -| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio | +| Preset (config) | UI Label | Usage | Notes | +| -------------------------------- | ------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| preset-record-generic | Record (Generic, no audio) | Record without audio | Use this if your camera has no audio, or if you don't want to record audio | +| preset-record-generic-audio-copy | Record (Generic + Copy Audio) | Record with the original audio | Use this to keep the camera's audio in recordings without re-encoding | +| preset-record-generic-audio-aac | Record (Generic + Audio to AAC) | Record with audio transcoded to AAC | The default when no output args are specified. Transcodes audio to AAC. If the source is already AAC, use `preset-record-generic-audio-copy` to avoid re-encoding | +| preset-record-mjpeg | Record - MJPEG Cameras | Record an MJPEG stream | Restreaming the MJPEG stream is recommended instead | +| preset-record-jpeg | Record - JPEG Cameras | Record a live JPEG | Restreaming the live JPEG is recommended instead | +| preset-record-ubiquiti | Record - Ubiquiti Cameras | Record a Ubiquiti stream with audio | Handles Ubiquiti's non-standard audio format | diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md index 9f396d3ccc..738eb5db3f 100644 --- a/docs/docs/configuration/genai/config.md +++ b/docs/docs/configuration/genai/config.md @@ -27,13 +27,12 @@ Running Generative AI models on CPU is not recommended, as high inference times You must use a vision-capable model with Frigate. The following models are recommended for local deployment: -| Model | Notes | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. | -| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. | -| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. | -| `Intern3.5VL` | Relatively fast with good vision comprehension | -| `gemma3` | Slower model with good vision and temporal understanding | +| Model | Notes | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. | +| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. | +| `qwen3.6` | Strong situational understanding, similar to qwen3-vl | +| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. | :::info @@ -56,7 +55,7 @@ Frigate manages reasoning per task automatically: - **Description tasks** (object descriptions, review descriptions, review summaries) are synthesis-only and benefit from concise, direct output, so Frigate disables thinking for these calls when the model exposes a per-request toggle. - **Chat** lets you toggle thinking on or off from the composer when the configured model supports it. -You can use a pure instruct, hybrid, or thinking-capable model with Frigate — no extra configuration is required to disable thinking for descriptions. +You can use a pure instruct, hybrid, or thinking-capable model with Frigate. No extra configuration is required to disable thinking for descriptions. ### llama.cpp @@ -294,7 +293,7 @@ Other HTTP options are available, see the [python-genai documentation](https://g ### OpenAI -OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route. +OpenAI does not have a free tier for their API. #### Supported Models diff --git a/docs/docs/configuration/go2rtc.md b/docs/docs/configuration/go2rtc.md index 08cc944a6f..c7e5b083bb 100644 --- a/docs/docs/configuration/go2rtc.md +++ b/docs/docs/configuration/go2rtc.md @@ -15,7 +15,7 @@ Frigate uses the bundled go2rtc to power a number of key features: :::tip[Most users no longer need to configure go2rtc by hand] -The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in , and the wizard probes your camera and writes its configuration for you — including the go2rtc restream and the live stream mapping — so go2rtc is set up automatically. +The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in , and the wizard probes your camera and writes its configuration for you, including the go2rtc restream and the live stream mapping, so go2rtc is set up automatically. This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added. @@ -23,9 +23,9 @@ This guide is mainly useful if you are **upgrading from an older version and hav ## Adding a go2rtc stream manually -If you added your cameras with the wizard, go2rtc is already configured — you can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand. +If you added your cameras with the wizard, go2rtc is already configured. You can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand. -Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#module-streams), not just rtsp. +Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#module-streams), not just rtsp. :::tip @@ -63,8 +63,8 @@ After adding this to the config, restart Frigate and try to watch the live strea ## Troubleshooting -If your stream won't play, has no audio, uses excessive CPU, or otherwise misbehaves, see the dedicated [go2rtc troubleshooting guide](/troubleshooting/go2rtc). It walks through how to isolate where the problem is and covers the most common issues — unsupported codecs, H.265/HEVC, audio, WebRTC and two-way talk, hardware-accelerated transcoding with FFmpeg 8, and camera-specific quirks. +If your stream won't play, has no audio, uses excessive CPU, or otherwise misbehaves, see the dedicated [go2rtc troubleshooting guide](/troubleshooting/go2rtc). It walks through how to isolate where the problem is and covers the most common issues: unsupported codecs, H.265/HEVC, audio, WebRTC and two-way talk, hardware-accelerated transcoding with FFmpeg 8, and camera-specific quirks. ## Homekit Configuration -To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`. +To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`. diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md index 66ee65545f..b9312c327f 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -17,8 +17,6 @@ Some types of hardware acceleration are detected and used automatically, but you - Check the logs: A message will either say that hardware acceleration was automatically detected, or there will be a warning that no hardware acceleration was automatically detected - If hardware acceleration is specified in the config, verification can be done by ensuring the logs are free from errors. There is no CPU fallback for hardware acceleration. -:::info - Frigate supports presets for optimal hardware accelerated video decoding: **AMD** @@ -49,14 +47,10 @@ Frigate supports presets for optimal hardware accelerated video decoding: Depending on your system, these presets may not be compatible, and you may need to use manual hwaccel args to take advantage of your hardware. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro -::: - ## Intel-based CPUs Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video decoding. -:::info - **Recommended hwaccel Preset** | CPU Generation | Intel Driver | Recommended Preset | Notes | @@ -68,8 +62,6 @@ Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video | Intel Arc A-series | iHD / Xe | preset-intel-qsv-\* | | | Intel Arc B-series | iHD / Xe | preset-intel-qsv-\* | Requires host kernel 6.12+ | -::: - :::note The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars). @@ -85,7 +77,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo -Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . @@ -105,7 +97,7 @@ ffmpeg: -Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to . @@ -123,7 +115,7 @@ ffmpeg: -Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to . @@ -178,7 +170,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo -Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to . @@ -237,7 +229,7 @@ Using `preset-nvidia` ffmpeg will automatically select the necessary profile for -Navigate to and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to . @@ -300,7 +292,7 @@ If you are using the HA App, you may need to use the full access variant and tur -Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to . @@ -420,7 +412,7 @@ For example, for H264 video, you'll select `preset-jetson-h264`. -Navigate to and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to . @@ -452,7 +444,7 @@ Set the FFmpeg hwaccel preset to enable hardware video processing. -Navigate to and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to . +Navigate to and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to . @@ -485,7 +477,7 @@ Error marking filters as finished Restarting ffmpeg... ``` -you should try to uprade to FFmpeg 7. This can be done using this config option: +you should try to upgrade to FFmpeg 7. This can be done using this config option: ```yaml ffmpeg: @@ -519,7 +511,7 @@ Set the FFmpeg hwaccel args to enable hardware video processing. -Navigate to and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to . +Navigate to and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to . diff --git a/docs/docs/configuration/license_plate_recognition.md b/docs/docs/configuration/license_plate_recognition.md index c60618fd43..eae92a9815 100644 --- a/docs/docs/configuration/license_plate_recognition.md +++ b/docs/docs/configuration/license_plate_recognition.md @@ -6,6 +6,7 @@ title: License Plate Recognition (LPR) import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; Frigate can recognize license plates on vehicles and automatically add the detected characters to the `recognized_license_plate` field or a [known](#matching) name as a `sub_label` to tracked objects of type `car` or `motorcycle`. A common use case may be to read the license plates of cars pulling into a driveway or cars passing by on a street. @@ -283,8 +284,8 @@ Navigate to Wife's Car** | `ABC-1234`, `ABC-I234` (accounts for potential confusion between the number one and capital letter I) | | **Known plates > Johnny** | `J*N-*234` (matches JHN-1234 and JMN-I234; `*` matches any number of characters) | | **Known plates > Sally** | `[S5]LL 1234` (matches both SLL 1234 and 5LL 1234) | @@ -363,7 +364,7 @@ An example configuration for a dedicated LPR camera using a `license_plate`-dete Navigate to and set **Enable LPR** to on. Set **Device** to `CPU` (can also be `GPU` if available). -Navigate to and add your camera streams. +Navigate to and add your camera streams. Navigate to . @@ -473,15 +474,15 @@ Navigate to Camera configuration > FFmpeg" /> and add your camera streams. +Navigate to and add your camera streams. Navigate to . | Field | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| **Enable object detection** | Set to off — disables Frigate's standard object detection pipeline | +| **Enable object detection** | Set to off to disable Frigate's standard object detection pipeline | | **Detect FPS** | Set to `5`. Increase if necessary, though high values may slow down Frigate's enrichments pipeline and use considerable CPU. | | **Detect width** | Set to `1920` (recommended value, but depends on your camera) | | **Detect height** | Set to `1080` (recommended value, but depends on your camera) | @@ -490,7 +491,7 @@ Navigate to . | Field | Description | | -------------------- | -------------------------------------------------------------------------------------- | -| **Objects to track** | Set to an empty list — required when not using a Frigate+ model for dedicated LPR mode | +| **Objects to track** | Set to an empty list, required when not using a Frigate+ model for dedicated LPR mode | Navigate to . @@ -591,7 +592,9 @@ By selecting the appropriate configuration, users can optimize their dedicated L ## FAQ -### Why isn't my license plate being detected and recognized? +### Detection and Recognition + + Ensure that: @@ -606,29 +609,43 @@ Recognized plates will show as object labels in the debug view and will appear i If you are still having issues detecting plates, start with a basic configuration and see the debugging tips below. -### Can I run LPR without detecting `car` or `motorcycle` objects? + + +Can I run LPR without detecting car or motorcycle objects?}> In normal LPR mode, Frigate requires a `car` or `motorcycle` to be detected first before recognizing a license plate. If you have a dedicated LPR camera, you can change the camera `type` to `"lpr"` to use the Dedicated LPR Camera algorithm. This comes with important caveats, though. See the [Dedicated LPR Cameras](#dedicated-lpr-cameras) section above. -### How can I improve detection accuracy? + + + - Use high-quality cameras with good resolution. - Adjust `detection_threshold` and `recognition_threshold` values. - Define a `format` regex to filter out invalid detections. -### Does LPR work at night? + + + Yes, but performance depends on camera quality, lighting, and infrared capabilities. Make sure your camera can capture clear images of plates at night. -### Can I limit LPR to specific zones? + + + LPR, like other Frigate enrichments, runs at the camera level rather than the zone level. While you can't restrict LPR to specific zones directly, you can control when recognition runs by setting a `min_area` value to filter out smaller detections. -### How can I match known plates with minor variations? + + + Use `match_distance` to allow small character mismatches. Alternatively, define multiple variations in `known_plates`. -### How do I debug LPR issues? + + +### Performance and Troubleshooting + + Start with ["Why isn't my license plate being detected and recognized?"](#why-isnt-my-license-plate-being-detected-and-recognized). If you are still having issues, work through these steps. @@ -671,7 +688,7 @@ lpr: 3. Ensure your plates are being _detected_. If you are using a Frigate+ or `license_plate` detecting model: - - Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected. + - Watch the [Debug view](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected. - View MQTT messages for `frigate/events` to verify detected plates. - You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected. @@ -680,21 +697,28 @@ lpr: - You may need to adjust your `detection_threshold` if your plates are not being detected. 4. Ensure the characters on detected plates are being _recognized_. + - Check the **Plate recognition** inference time in Enrichment metrics (). High inference times (> 100ms) could lead to poor recognition results, especially for dedicated LPR cameras where the plate crosses the frame quickly. - Enable `debug_save_plates` to save images of detected text on plates to the clips directory (`/media/frigate/clips/lpr`). Ensure these images are readable and the text is clear. - Watch the debug view to see plates recognized in real-time. For non-dedicated LPR cameras, the `car` or `motorcycle` label will change to the recognized plate when LPR is enabled and working. - Adjust `recognition_threshold` settings per the suggestions [above](#advanced-configuration). -### Will LPR slow down my system? + + + LPR's performance impact depends on your hardware. Ensure you have at least 4GB RAM and a capable CPU or GPU for optimal results. If you are running the Dedicated LPR Camera mode, resource usage will be higher compared to users who run a model that natively detects license plates. Tune your motion detection settings for your dedicated LPR camera so that the license plate detection model runs only when necessary. -### I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects `license_plate`. Why is the YOLOv9 model running? + + +I am seeing a YOLOv9 plate detection metric in Enrichment Metrics, but I have a Frigate+ or custom model that detects license_plate. Why is the YOLOv9 model running?}> The YOLOv9 license plate detector model will run (and the metric will appear) if you've enabled LPR but haven't defined `license_plate` as an object to track, either at the global or camera level. If you are detecting `car` or `motorcycle` on cameras where you don't want to run LPR, make sure you disable LPR it at the camera level. And if you do want to run LPR on those cameras, make sure you define `license_plate` as an object to track. -### It looks like Frigate picked up my camera's timestamp or overlay text as the license plate. How can I prevent this? + + + This could happen if cars or motorcycles travel close to your camera's timestamp or overlay text. You could either move the text through your camera's firmware, or apply a mask to it in Frigate. @@ -702,6 +726,10 @@ If you are using a model that natively detects `license_plate`, add an _object m If you are not using a model that natively detects `license_plate` or you are using dedicated LPR camera mode, only a _motion mask_ over your text is required. -### I see "Error running ... model" in my logs, or my inference time is very high. How can I fix this? + + + This usually happens when your GPU is unable to compile or use one of the LPR models. Set your `device` to `CPU` and try again. GPU acceleration only provides a slight performance increase, and the models are lightweight enough to run without issue on most CPUs. + + diff --git a/docs/docs/configuration/live.md b/docs/docs/configuration/live.md index effe89de3c..5df70844c9 100644 --- a/docs/docs/configuration/live.md +++ b/docs/docs/configuration/live.md @@ -6,6 +6,7 @@ title: Live View import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; +import FaqItem from "@site/src/components/FaqItem"; Frigate intelligently displays your camera streams on the Live view dashboard. By default, Frigate employs "smart streaming" where camera images update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any motion or active objects are detected, cameras seamlessly switch to a live stream. @@ -271,9 +272,9 @@ cameras: Each camera has three possible states, surfaced as a status selector in **Settings → Global configuration → Camera management**: -- **On** — streams are processed normally. Object detection, recording, and Live view are active. -- **Off** — Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. The Off state persists across Frigate restarts via a `.runtime_state.json` file alongside `config.yml` (see [Runtime toggle persistence](#runtime-toggle-persistence)). -- **Disabled** — the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.** +- **On**: streams are processed normally. Object detection, recording, and Live view are active. +- **Off**: Frigate's ffmpeg processes are paused. Recording stops, object detection is paused, and the Live dashboard displays a blank image with a "Camera is off" message. The camera is still visible in the Live dashboard and its past review items, tracked objects, and historical footage remain accessible via the UI. The Off state persists across Frigate restarts via a `.runtime_state.json` file alongside `config.yml` (see [Runtime toggle persistence](#runtime-toggle-persistence)). +- **Disabled**: the change is saved to your configuration file (`enabled: False`). The camera stops immediately, Frigate stops ffmpeg processes, and all live and historical UI elements for the camera are no longer visible but remains retained on disk. The camera is still listed in **Settings → Global configuration → Camera management** so it can be re-enabled. **A restart of Frigate is required to bring a disabled camera back to On.** #### Turning a camera on or off @@ -302,7 +303,7 @@ If you want a camera's historical data (review items, tracked objects, footage) #### Runtime toggle persistence -The Live view toggles for **camera on/off**, **detect**, **recordings**, **snapshots**, and **audio detection** — along with the equivalent MQTT `/set` topics — write the new state to `.runtime_state.json` next to your `config.yml`. The file is replayed on Frigate startup so your last-known toggle states survive a restart. Two interactions worth knowing: +The Live view toggles for **camera on/off**, **detect**, **recordings**, **snapshots**, and **audio detection** (along with the equivalent MQTT `/set` topics) write the new state to `.runtime_state.json` next to your `config.yml`. The file is replayed on Frigate startup so your last-known toggle states survive a restart. Two interactions worth knowing: - **Settings UI saves win.** When you save a field through **Settings → Global configuration**, the matching entry is cleared from `.runtime_state.json` so the new value in your config file is the durable source. - **Switching profiles clears all runtime overrides.** Activating or deactivating a [profile](/configuration/profiles) is treated as a deliberate state change, so the file is wiped to avoid stale overrides replaying on top of the new profile. @@ -333,7 +334,7 @@ When your browser runs into problems playing back your camera streams, it will l - **stalled** - What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving). - - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval — shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings. + - What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in the UI pane of Frigate's settings. - Possible console messages from the player code: - `Buffer time (10 seconds) exceeded, browser may not be playing media correctly.` @@ -341,94 +342,155 @@ When your browser runs into problems playing back your camera streams, it will l ## Live view FAQ -1. **Why don't I have audio in my Live view?** +### Getting Live View Working - You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. + - Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. +You must use go2rtc to hear audio in your live streams. If you have go2rtc already configured, you need to ensure your camera is sending PCMA/PCMU or AAC audio. If you can't change your camera's audio codec, you need to [transcode the audio](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#source-ffmpeg) using go2rtc. -2. **Frigate shows that my live stream is in "low bandwidth mode". What does this mean?** +If the audio controls don't appear in the UI at all, verify that the Live view is actually using your go2rtc stream. If your go2rtc stream names don't match your Frigate camera name, you must map them with the `live -> streams` config (see [Setting Streams For Live UI](#setting-streams-for-live-ui) above); otherwise the UI falls back to the video-only jsmpeg player. - Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. +Note that the low bandwidth mode player is a video-only stream. You should not expect to hear audio when in low bandwidth mode, even if you've set up go2rtc. - When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. + - If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. + - Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: - - Network issues (e.g., MSE or WebRTC network connection problems). - - Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). - - Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. - - Browser compatibility problems (e.g., iOS Safari limitations with MSE). +If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. - To view browser console logs: - 1. Open the Frigate Live View in your browser. - 2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). - 3. Reproduce the error (e.g., load a problematic stream or simulate network issues). - 4. Look for messages prefixed with the camera name. + - These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: - - Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)). - - Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). - - Test with a different stream via the UI dropdown (if `live -> streams` is configured). - - For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see (WebRTC Extra Configuration)(#webrtc-extra-configuration)). - - If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + -3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?** +The debug view plays the `detect` stream processed by Frigate itself, while the Live view plays your go2rtc stream directly in the browser. If the debug view works but the Live view doesn't, your browser usually can't decode what the camera is sending, most often H.265 video or an incompatible audio track. - On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. +Work through the [go2rtc troubleshooting guide](/troubleshooting/go2rtc#live-view-is-black-buffering-or-stuck-in-low-bandwidth-mode) to isolate the problem. Two fixes resolve the majority of cases: -4. **I see a strange diagonal line on my live view, but my recordings look fine. How can I fix it?** +1. Restream through go2rtc's FFmpeg module by prefixing your source with `ffmpeg:`, for example `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream`. +2. If that doesn't help, transcode to compatible codecs: `- ffmpeg:rtsp://user:password@192.168.1.5:554/stream#video=h264#audio=aac#hardware`. - This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + -5. **How does "smart streaming" work?** + - Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. +For a full-resolution, low-latency live view in Home Assistant dashboards, use the [Advanced Camera Card](https://card.camera) with the [go2rtc live provider](https://card.camera/#/configuration/cameras/live-provider?id=go2rtc), which streams directly from Frigate's bundled go2rtc. This also supports audio and [two-way talk](#two-way-talk) on capable cameras. See the [Home Assistant integration docs](/integrations/home-assistant) for setup. - This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. + - Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. +### Streaming Behavior - This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. + -6. **I have unmuted some cameras on my dashboard, but I do not hear sound. Why?** +Because a static image of a scene looks exactly the same as a live stream with no motion or activity, smart streaming updates your camera images once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity (motion or object/audio detection) occurs, cameras seamlessly switch to a live stream. - If your camera is streaming (as indicated by a red dot in the upper right, or if it has been set to continuous streaming mode), your browser may be blocking audio until you interact with the page. This is an intentional browser limitation. See [this article](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability). Many browsers have a whitelist feature to change this behavior. +This static image is pulled from the stream defined in your config with the `detect` role. When activity is detected, images from the `detect` stream immediately begin updating at ~5 frames per second so you can see the activity until the live player is loaded and begins playing. This usually only takes a second or two. If the live player times out, buffers, or has streaming errors, the jsmpeg player is loaded and plays a video-only stream from the `detect` role. When activity ends, the players are destroyed and a static image is displayed until activity is detected again, and the process repeats. -7. **My camera streams have lots of visual artifacts / distortion.** +Smart streaming depends on having your camera's motion `threshold` and `contour_area` config values dialed in. Use the Motion Tuner in Settings in the UI to tune these values in real-time. - Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. +This is Frigate's default and recommended setting because it results in a significant bandwidth savings, especially for high resolution cameras. -8. **Why does my camera stream switch aspect ratios on the Live dashboard?** + - Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + - To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. +On the default Live dashboard ("All Cameras"), your camera images will update once per minute when no detectable activity is occurring to conserve bandwidth and resources. As soon as any activity is detected, cameras seamlessly switch to a full-resolution live stream. If you want to customize this behavior, use a camera group. - Example: Resolutions from two streams - - Mismatched (may cause aspect ratio switching on the dashboard): - - Live/go2rtc stream: 1920x1080 (16:9) - - Detect stream: 640x352 (~1.82:1, not 16:9) + - - Matched (prevents switching): - - Live/go2rtc stream: 1920x1080 (16:9) - - Detect stream: 640x360 (16:9) + - You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: +Frigate intelligently selects the live streaming technology based on a number of factors (user-selected modes like two-way talk, camera settings, browser capabilities, available bandwidth) and prioritizes showing an actual up-to-date live view of your camera's stream as quickly as possible. - ```yaml - cameras: - front_door: - detect: - width: 640 - height: 360 # set this to 360 instead of 352 - ffmpeg: - inputs: - - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 - roles: - - record - - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 - roles: - - detect - ``` +When you have go2rtc configured, Live view initially attempts to load and play back your stream with a clearer, fluent stream technology (MSE). An initial timeout, a low bandwidth condition that would cause buffering of the stream, or decoding errors in the stream will cause Frigate to switch to the stream defined by the `detect` role, using the jsmpeg format. This is what the UI labels as "low bandwidth mode". On Live dashboards, the mode will automatically reset when smart streaming is configured and activity stops. Continuous streaming mode does not have an automatic reset mechanism, but you can use the _Reset_ option to force a reload of your stream. + +If you are using continuous streaming or you are loading more than a few high resolution streams at once on the dashboard, your browser may struggle to begin playback of your streams before the timeout. Frigate always prioritizes showing a live stream as quickly as possible, even if it is a lower quality jsmpeg stream. You can use the "Reset" link/button to try loading your high resolution stream again. + +Errors in stream playback (e.g., connection failures, codec issues, or buffering timeouts) that cause the fallback to low bandwidth mode (jsmpeg) are logged to the browser console for easier debugging. These errors may include: + +- Network issues (e.g., MSE or WebRTC network connection problems). +- Unsupported codecs or stream formats (e.g., H.265 in WebRTC, which is not supported in some browsers). +- Buffering timeouts or low bandwidth conditions causing fallback to jsmpeg. +- Browser compatibility problems (e.g., iOS Safari limitations with MSE). + +To view browser console logs: + +1. Open the Frigate Live View in your browser. +2. Open the browser's Developer Tools (F12 or right-click > Inspect > Console tab). +3. Reproduce the error (e.g., load a problematic stream or simulate network issues). +4. Look for messages prefixed with the camera name. + +These logs help identify if the issue is player-specific (MSE vs. WebRTC) or related to camera configuration (e.g., go2rtc streams, codecs). If you see frequent errors: + +- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)). +- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS). +- Test with a different stream via the UI dropdown (if `live -> streams` is configured). +- For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)). +- If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream. + + + + + +A delay when a stream first starts is usually caused by your camera's I-frame (keyframe) interval. Playback cannot begin until a keyframe arrives, so an interval set higher than your camera's frame rate makes the stream take longer to start. Set the I-frame interval to match the frame rate (or "1x" on Reolink) per the [camera settings recommendations](#camera-settings-recommendations). + +A stream that starts on time but falls further behind live is buffering, which is usually the browser struggling to decode too many high-resolution streams at once. Select a lower-bandwidth substream for your dashboards (see [Setting Streams For Live UI](#setting-streams-for-live-ui)), reduce the number of streams open at once, or improve the network connection between your browser and Frigate. Frigate's player automatically speeds up playback to catch up to live after buffering, and falls back to low bandwidth mode if it stalls for too long. The _Reset_ option forces a fresh connection at the live edge. + + + + + +Frigate prefers MSE because it delivers a better out-of-the-box experience than WebRTC on nearly every axis that matters for a security camera system. MSE is an open standard optimized and supported by all modern browsers, works without any extra configuration (WebRTC requires port forwarding and candidate setup, and lacks H.265 support in some browsers), and requires no internet access for NAT traversal. More importantly, MSE runs over TCP, so every frame arrives and is decoded in order, so nothing is ever silently skipped. WebRTC optimizes for latency over UDP by discarding late or incomplete frames, which works against you on cellular or spotty Wi-Fi: you can end up with frozen video, visual corruption, or gaps in the feed without ever knowing you missed something. Frigate's enhanced MSE player has adaptive speed playback and has been tuned for latency and connection robustness that meets or exceeds WebRTC, so you get near-real-time playback with a guarantee that when the video plays, every frame is actually there - which, for an NVR whose whole purpose is letting you see what happened, matters more than shaving fractions of a second off a latency number. That's why Frigate defaults to MSE and reserves WebRTC for cases that require it, like two-way talk. + + + +### Video Quality Issues + + + +This is caused by incorrect dimensions set in your detect width or height (or incorrectly auto-detected), causing the jsmpeg player's rendering engine to display a slightly distorted image. You should enlarge the width and height of your `detect` resolution up to a standard aspect ratio (example: 640x352 becomes 640x360, and 800x443 becomes 800x450, 2688x1520 becomes 2688x1512, etc). If changing the resolution to match a standard (4:3, 16:9, or 32:9, etc) aspect ratio does not solve the issue, you can enable "compatibility mode" in your camera group dashboard's stream settings. Depending on your browser and device, more than a few cameras in compatibility mode may not be supported, so only use this option if changing your `detect` width and height fails to resolve the color artifacts and diagonal line. + + + + + +Some cameras don't include the hardware to support multiple connections to the high resolution stream, and this can cause unexpected behavior. In this case it is recommended to [restream](./restream.md) the high resolution stream so that it can be used for live view and recordings. + + + + + +Your camera may change aspect ratios on the dashboard because Frigate uses different streams for different purposes. With go2rtc and Smart Streaming, Frigate shows a static image from the `detect` stream when no activity is present, and switches to the live stream when motion is detected. The camera image will change size if your streams use different aspect ratios. + +To prevent this, make the `detect` stream match the go2rtc live stream's aspect ratio (resolution does not need to match, just the aspect ratio). You can either adjust the camera's output resolution or set the `width` and `height` values in your config's `detect` section to a resolution with an aspect ratio that matches. + +Example: Resolutions from two streams + +- Mismatched (may cause aspect ratio switching on the dashboard): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x352 (~1.82:1, not 16:9) + +- Matched (prevents switching): + - Live/go2rtc stream: 1920x1080 (16:9) + - Detect stream: 640x360 (16:9) + +You can update the detect settings in your camera config to match the aspect ratio of your go2rtc live stream. For example: + +```yaml +cameras: + front_door: + detect: + width: 640 + height: 360 # set this to 360 instead of 352 + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/front_door # main stream 1920x1080 + roles: + - record + - path: rtsp://127.0.0.1:8554/front_door_sub # sub stream 640x352 + roles: + - detect +``` + +The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ). + + diff --git a/docs/docs/configuration/masks.md b/docs/docs/configuration/masks.md index e497de2c19..50c9fffc31 100644 --- a/docs/docs/configuration/masks.md +++ b/docs/docs/configuration/masks.md @@ -7,9 +7,11 @@ 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`](zones.md#restricting-alerts-and-detections-to-specific-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._ +Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the [Debug view](/usage/live#the-single-camera-view) 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._ See [further clarification](#further-clarification) below on why you may not want to use a motion mask. @@ -21,7 +23,16 @@ Object filter masks can be used to filter out stubborn false positives in fixed ![object mask](/img/bottom-center-mask.jpg) -## Creating masks +## Which tool do I need? + +| What you're trying to do | Recommended tool | How it works | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Only get alerts/detections for activity in the areas you care about, ignoring activity elsewhere (e.g., alert when someone enters your yard, but not when they walk past on the sidewalk) | A [zone](zones.md) combined with [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-zones) | Frigate keeps detecting and tracking activity everywhere in the frame, 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 @@ -124,3 +135,14 @@ This is what `required_zones` are for. You should define a zone (remember this i > Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image. That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway. + +## Common mistakes + +**"I added a motion mask to ignore my driveway/sidewalk."** +A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to [`required_zones`](zones.md#restricting-alerts-and-detections-to-specific-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`](zones.md#restricting-alerts-and-detections-to-specific-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`](zones.md#restricting-alerts-and-detections-to-specific-zones) for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact. diff --git a/docs/docs/configuration/metrics.md b/docs/docs/configuration/metrics.md index d857d5eeee..69db159cc5 100644 --- a/docs/docs/configuration/metrics.md +++ b/docs/docs/configuration/metrics.md @@ -59,6 +59,8 @@ Metrics are available at `/api/metrics` by default. No additional Frigate config - `frigate_storage_used_bytes{storage=""}` - Storage used bytes - `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info +These gauges report the operating system's figures for the whole filesystem (the same numbers as `df`), not Frigate's own recording footprint. For how this differs from the recordings usage shown in the UI, see [Understanding storage usage](/configuration/record#understanding-storage-usage). + ### Service Metrics - `frigate_service_uptime_seconds` - Uptime in seconds diff --git a/docs/docs/configuration/motion_detection.md b/docs/docs/configuration/motion_detection.md index 7a4a70d027..a7928954e2 100644 --- a/docs/docs/configuration/motion_detection.md +++ b/docs/docs/configuration/motion_detection.md @@ -11,7 +11,7 @@ import NavPath from "@site/src/components/NavPath"; Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection. -Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer. +Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the [debug viewer](/usage/live#the-single-camera-view). ## The Goal @@ -66,7 +66,7 @@ motion: -Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dogs blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion. +Lower values mean motion detection is more sensitive to changes in color, making it more likely for example to detect motion when a brown dog blends in with a brown fence or a person wearing a red shirt blends in with a red car. If the threshold is too low however, it may detect things like grass blowing in the wind, shadows, etc. to be detected as motion. Watching the motion boxes in the debug view, increase the threshold until you only see motion that is visible to the eye. Once this is done, it is important to test and ensure that desired motion is still detected. @@ -151,7 +151,7 @@ motion: Large changes in motion like PTZ moves and camera switches between Color and IR mode should result in a pause in object detection. `lightning_threshold` defines the percentage of the image used to detect these substantial changes. Increasing this value makes motion detection more likely to treat large changes (like IR mode switches) as valid motion. Decreasing it makes motion detection more likely to ignore large amounts of motion, such as a person approaching a doorbell camera. -Note that `lightning_threshold` does **not** stop motion-based recordings from being saved — it only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings. +Note that `lightning_threshold` does **not** stop motion-based recordings from being saved. It only prevents additional motion analysis after the threshold is exceeded, reducing false positive object detections during high-motion periods (e.g. storms or PTZ sweeps) without interfering with recordings. :::warning @@ -194,10 +194,10 @@ This option is handy when you want to prevent large transient changes from trigg :::warning -When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise — they typically only take up a few megabytes and are quick to scan in the timeline UI. +When the skip threshold is exceeded, **no motion is reported** for that frame, meaning **nothing is recorded** for that frame. That means you can miss something important, like a PTZ camera auto-tracking an object or activity while the camera is moving. If you prefer to guarantee that every frame is saved, leave this unset and accept occasional recordings containing scene noise. They typically only take up a few megabytes and are quick to scan in the timeline UI. ::: ## Reviewing Detected Motion -To review what the detector picked up — or to search past recordings for motion in a specific region — see [Reviewing Motion](/usage/review#reviewing-motion) on the Review page. +To review what the detector picked up, or to search past recordings for motion in a specific region, see [Reviewing Motion](/usage/review#reviewing-motion) on the Review page. diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index e4a5082322..23595ac6e8 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -4,14 +4,19 @@ title: Object Detectors --- import CommunityBadge from '@site/src/components/CommunityBadge'; -import ConfigTabs from "@site/src/components/ConfigTabs"; -import TabItem from "@theme/TabItem"; -import NavPath from "@site/src/components/NavPath"; +import ConfigTabs from '@site/src/components/ConfigTabs'; +import TabItem from '@theme/TabItem'; +import NavPath from '@site/src/components/NavPath'; +import ModelConfigDropdown from '@site/src/components/ModelConfigDropdown'; +import objectDetectorsModels from '@site/data/object_detectors_models.yaml'; -# Supported Hardware +### Supported hardware + +Object detection is what allows Frigate to identify _what_ is in your camera's view (people, cars, animals, and more) rather than just reacting to pixel changes. When Frigate's motion detection finds activity in a frame, that region is sent to an **object detector**, which returns the objects it recognizes along with their location and a confidence score. These detections are what drive tracked objects, alerts, detections, and notifications. + +Object detection is computationally intensive, so Frigate is designed to run it on a dedicated AI accelerator or GPU rather than the CPU. A **detector** is the specific hardware-and-model backend Frigate uses to run inference. Choosing a detector that matches your hardware is one of the most important steps in getting good performance, and the right choice depends on what device Frigate is running on. :::info - Frigate supports multiple different detectors that work on different types of hardware: **Most Hardware** @@ -70,6 +75,22 @@ This does not affect using hardware for accelerating other tasks such as [semant ::: +### Choosing a model size + +Along with picking a detector for your hardware, you will choose a model's **input resolution** (such as `320x320` or `640x640`) and, for model families like YOLOv9, a **variant size** (`tiny`, `small`, etc.). Both affect the balance between accuracy and the inference time your hardware can sustain. + +**Resolution (320x320 vs 640x640):** Frigate is optimized for `320x320` models, and `320x320` is the best choice for the vast majority of setups. Frigate is specifically designed to compensate for the smaller model by cropping a region of motion from the full frame and zooming into it before running detection, so a `320x320` model is actually _better_ at small and distant objects, not worse. A `640x640` model is slower and uses more resources, and its main benefit is fitting more objects into a single inference when many objects are spread across a large area. Recent versions of Frigate have improved support for `640x640` models, but `320x320` remains the recommended starting point for nearly all setups. + +**Variant size (tiny/small/medium):** Larger variants are gradually more accurate but slower. Whether the difference is noticeable depends on your specific cameras and scenes. A good rule of thumb is to use the largest model your hardware can run without skipping detections, which you can monitor on the page in the UI. Better accuracy only helps if your detector keeps up with the detection load across all cameras. + +**Acceptable inference time depends on your hardware.** Inference time alone does not tell the whole story, because different hardware has different capacity. A GPU can run multiple instances of the same model concurrently, so an inference time around 30ms can still keep up with several cameras. A Google Coral runs only a single instance of the model, so it needs a much lower inference time (around 10ms) to keep up. + +:::tip + +The best detection accuracy comes from a model trained on images that look like what Frigate actually sees: security camera footage cropped to regions of interest. You can train or fine-tune your own model on images like this and run it as a custom model (see the per-detector sections below), but [Frigate+](/plus) makes this much easier by handling the training for you on images submitted from your own cameras. For YOLOv9, the `s` (small) variant at `320x320` resolution is a good place to start. + +::: + # Officially Supported Detectors Frigate provides a number of builtin detector types. By default, Frigate will use a single CPU detector. Other detectors may require additional configuration as described below. When using multiple detectors they will run in dedicated processes, but pull from a common queue of detection requests from across all cameras. @@ -217,68 +238,9 @@ detectors: -### EdgeTPU Supported Models +### Configuration {#configuration-edgetpu} -| Model | Notes | -| ----------------------- | ------------------------------------------- | -| [Mobiledet](#mobiledet) | Default model | -| [YOLOv9](#yolov9) | More accurate but slower than default model | - -#### Mobiledet - -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`. - -#### YOLOv9 - -YOLOv9 models that are compiled for TensorFlow Lite and properly quantized are supported, but not included by default. [Instructions](#yolov9-for-google-coral-support) for downloading a model with support for the Google Coral. - -:::tip - -**Frigate+ Users:** Follow the [instructions](/integrations/plus#use-models) to set a model ID in your config file. - -::: - -
- YOLOv9 Setup & Config - -After placing the downloaded files for the tflite model and labels in your config folder, use the following configuration: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ----------------------------------------------------------------- | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` (should match the imgsize of the model) | -| **Object detection model input height** | `320` (should match the imgsize of the model) | -| **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` | -| **Label map for custom object detector** | `/config/labels-coco17.txt` | - - - - -```yaml -detectors: - coral: - type: edgetpu - device: usb - -model: - model_type: yolo-generic - width: 320 # <--- should match the imgsize of the model, typically 320 - height: 320 # <--- should match the imgsize of the model, typically 320 - path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite - labelmap_path: /config/labels-coco17.txt -``` - - - - -Note that due to hardware limitations of the Coral, the labelmap is a subset of the COCO labels and includes only 17 object classes. - -
+ --- @@ -294,150 +256,12 @@ If no custom model is provided, the Hailo detector downloads a default model fro ::: -### Configuration +### Configuration {#configuration-hailo} When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**. If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`. -#### YOLO - -Use this configuration for YOLO-based models. When no custom model path or URL is provided, the detector automatically downloads the default model based on the detected hardware: - -- **Hailo-8 hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`) -- **Hailo-8L hardware:** Uses **YOLOv6n** (default: `yolov6n.hef`) - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ----------------------- | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Tensor Shape** | `nhwc` | -| **Model Input Pixel Color Format** | `rgb` | -| **Model Input D Type** | `int` | -| **Object Detection Model Type** | `yolo-generic` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - -The detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override. - - - - -```yaml -detectors: - hailo: - type: hailo8l - device: PCIe - -model: - width: 320 - height: 320 - input_tensor: nhwc - input_pixel_format: rgb - input_dtype: int - model_type: yolo-generic - labelmap_path: /labelmap/coco-80.txt - - # The detector automatically selects the default model based on your hardware: - # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef) - # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef) - # - # Optionally, you can specify a local model path to override the default. - # If a local path is provided and the file exists, it will be used instead of downloading. - # Example: - # path: /config/model_cache/hailo/yolov6n.hef - # - # You can also override using a custom URL: - # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef - # just make sure to give it the write configuration based on the model -``` - - - - -#### SSD - -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. - - - - -Navigate to 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: - -| Field | Value | -| --------------------------------------- | ------ | -| **Object detection model input width** | `300` | -| **Object detection model input height** | `300` | -| **Model Input Tensor Shape** | `nhwc` | -| **Model Input Pixel Color Format** | `rgb` | -| **Object Detection Model Type** | `ssd` | - -Specify the local model path or URL for SSD MobileNet v1. - - - - -```yaml -detectors: - hailo: - type: hailo8l - device: PCIe - -model: - width: 300 - height: 300 - input_tensor: nhwc - input_pixel_format: rgb - model_type: ssd - # Specify the local model path (if available) or URL for SSD MobileNet v1. - # Example with a local path: - # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef - # - # Or override using a custom URL: - # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef -``` - - - - -#### Custom Models - -The Hailo detector supports all YOLO models compiled for Hailo hardware that include post-processing. You can specify a custom URL or a local path to download or use your model directly. If both are provided, the detector checks the local path first. - - - - -Navigate to 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 to match your custom model dimensions and format. - - - - -```yaml -detectors: - hailo: - type: hailo8l - device: PCIe - -model: - width: 640 - height: 640 - input_tensor: nhwc - input_pixel_format: rgb - input_dtype: int - model_type: yolo-generic - labelmap_path: /labelmap/coco-80.txt - # Optional: Specify a local model path. - # path: /config/model_cache/hailo/custom_model.hef - # - # Alternatively, or as a fallback, provide a custom URL: - # path: https://custom-model-url.com/path/to/model.hef -``` - - - + For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo @@ -462,14 +286,6 @@ OpenVINO is supported on 6th Gen Intel platforms (Skylake) and newer. It will al When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: - - - -Navigate to and select **OpenVINO** from the detector type dropdown and click **Add** to add multiple detectors, each targeting `GPU` or `NPU`. - - - - ```yaml detectors: ov_0: @@ -480,406 +296,34 @@ detectors: device: GPU # or NPU ``` - - - ::: -### OpenVINO Supported Models +### Configuration {#configuration-openvino} -| Model | GPU | NPU | Notes | -| ------------------------------------- | --- | --- | ------------------------------------------------------------ | -| [YOLOv9](#yolo-v3-v4-v7-v9) | ✅ | ✅ | Recommended for GPU & NPU | -| [RF-DETR](#rf-detr) | ✅ | ✅ | Requires XE iGPU or Arc | -| [YOLO-NAS](#yolo-nas) | ✅ | ✅ | | -| [MobileNet v2](#ssdlite-mobilenet-v2) | ✅ | ✅ | Fast and lightweight model, less accurate than larger models | -| [YOLOX](#yolox) | ✅ | ? | | -| [D-FINE / DEIMv2](#d-fine--deimv2) | ❌ | ❌ | | + -#### SSDLite MobileNet v2 - -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. - -
- MobileNet v2 Config - -Use the model configuration shown below when using the OpenVINO detector with the default OpenVINO model: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------ | -| **Object detection model input width** | `300` | -| **Object detection model input height** | `300` | -| **Model Input Tensor Shape** | `nhwc` | -| **Model Input Pixel Color Format** | `bgr` | -| **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` | -| **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` | - - - - -```yaml -detectors: - ov: - type: openvino - device: GPU # Or NPU - -model: - width: 300 - height: 300 - input_tensor: nhwc - input_pixel_format: bgr - path: /openvino-model/ssdlite_mobilenet_v2.xml - labelmap_path: /openvino-model/coco_91cl_bkgr.txt -``` - - - - -
- -#### YOLOX - -This detector also supports YOLOX. Frigate does not come with any YOLOX models preloaded, so you will need to supply your own models. - -#### YOLO-NAS - -[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. - -
- YOLO-NAS Setup & Config - -After placing the downloaded onnx model in your config folder, use the following configuration: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------------- | -| **Object Detection Model Type** | `yolonas` | -| **Object detection model input width** | `320` (should match whatever was set in notebook) | -| **Object detection model input height** | `320` (should match whatever was set in notebook) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input Pixel Color Format** | `bgr` | -| **Custom object detector model path** | `/config/yolo_nas_s.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - ov: - type: openvino - device: GPU - -model: - model_type: yolonas - width: 320 # <--- should match whatever was set in notebook - height: 320 # <--- should match whatever was set in notebook - input_tensor: nchw - input_pixel_format: bgr - path: /config/yolo_nas_s.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -
- -#### YOLO (v3, v4, v7, v9) - -YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. - -:::tip - -The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. - -::: - -
- YOLOv Setup & Config - -:::warning - -If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. - -::: - -After placing the downloaded onnx model in your config folder, use the following configuration: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | -------------------------------------------------------- | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` (should match the imgsize set during model export) | -| **Object detection model input height** | `320` (should match the imgsize set during model export) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/yolo.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - ov: - type: openvino - device: GPU # or NPU - -model: - model_type: yolo-generic - width: 320 # <--- should match the imgsize set during model export - height: 320 # <--- should match the imgsize set during model export - input_tensor: nchw - input_dtype: float - path: /config/model_cache/yolo.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -
- -#### RF-DETR - -[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more information on downloading the RF-DETR model for use in Frigate. - -:::warning - -Due to the size and complexity of the RF-DETR model, it is only recommended to be run with discrete Arc Graphics Cards. - -::: - -
- RF-DETR Setup & Config - -After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration: - - - - -Navigate to 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: - -| Field | Value | -| --------------------------------------- | --------------------------------- | -| **Object Detection Model Type** | `rfdetr` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` | - - - - -```yaml -detectors: - ov: - type: openvino - device: GPU - -model: - model_type: rfdetr - width: 320 - height: 320 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/rfdetr.onnx -``` - - - - -
- -#### D-FINE / DEIMv2 - -[D-FINE](https://github.com/Peterande/D-FINE) and [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) are DETR based models that share the same ONNX input/output format. The ONNX exported models are supported, but not included by default. See the models section for downloading [D-FINE](#downloading-d-fine-model) or [DEIMv2](#downloading-deimv2-model) for use in Frigate. - -:::warning - -Currently D-FINE / DEIMv2 models only run on OpenVINO in CPU mode, GPUs currently fail to compile the model - -::: - -
- D-FINE Setup & Config - -After placing the downloaded onnx model in your config/model_cache folder, use the following configuration: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ---------------------------------- | -| **Object Detection Model Type** | `dfine` | -| **Object detection model input width** | `640` | -| **Object detection model input height** | `640` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - ov: - type: openvino - device: CPU - -model: - model_type: dfine - width: 640 - height: 640 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/dfine-s.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -
- -
- DEIMv2 Setup & Config - -After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: - -```yaml -detectors: - ov: - type: openvino - device: CPU - -model: - model_type: dfine - width: 640 - height: 640 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/deimv2_hgnetv2_n.onnx - labelmap_path: /labelmap/coco-80.txt -``` - -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -
+--- ## Apple Silicon detector The NPU in Apple Silicon can't be accessed from within a container, so the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) must first be setup. It is recommended to use the Frigate docker image with `-standard-arm64` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-standard-arm64`. -### Setup +### Setup {#setup-apple-silicon} 1. Setup the [Apple Silicon detector client](https://github.com/frigate-nvr/apple-silicon-detector) and run the client 2. Configure the detector in Frigate and startup Frigate -### Configuration +### Configuration {#configuration-apple-silicon} Using the detector config below will connect to the client: - - - -Navigate to and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. - - - - -```yaml -detectors: - apple-silicon: - type: zmq - endpoint: tcp://host.docker.internal:5555 -``` - - - - -### Apple Silicon Supported Models - -There is no default model provided, the following formats are supported: - -#### YOLO (v3, v4, v7, v9) - -YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. - -:::tip - -The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. - -::: - -When Frigate is started with the following config it will connect to the detector client and transfer the model automatically: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | -------------------------------------------------------- | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` (should match the imgsize set during model export) | -| **Object detection model input height** | `320` (should match the imgsize set during model export) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/yolo.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - apple-silicon: - type: zmq - endpoint: tcp://host.docker.internal:5555 - -model: - model_type: yolo-generic - width: 320 # <--- should match the imgsize set during model export - height: 320 # <--- should match the imgsize set during model export - input_tensor: nchw - input_dtype: float - path: /config/model_cache/yolo.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - + Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. ## AMD/ROCm GPU detector -### Setup +### Setup {#setup-rocm} Support for AMD GPUs is provided using the [ONNX detector](#onnx). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`. @@ -957,7 +401,7 @@ We unset the `HSA_OVERRIDE_GFX_VERSION` to prevent an existing override from mes $ docker exec -it frigate /bin/bash -c '(unset HSA_OVERRIDE_GFX_VERSION && /opt/rocm/bin/rocminfo |grep gfx)' ``` -### ROCm Supported Models +### Configuration {#configuration-rocm} :::tip @@ -970,11 +414,13 @@ The AMD GPU kernel is known problematic especially when converting models to mxr ::: -See [ONNX supported models](#supported-models) for supported models, there are some caveats: +See [ONNX supported models](#onnx) for supported models, there are some caveats: - D-FINE / DEIMv2 models are not supported - YOLO-NAS models are known to not run well on integrated GPUs + + ## ONNX ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available. @@ -999,14 +445,6 @@ If the correct build is used for your GPU then the GPU will be detected and used When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming GPU resources are available. An example configuration would be: - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add** to add multiple detectors. - - - - ```yaml detectors: onnx_0: @@ -1015,301 +453,13 @@ detectors: type: onnx ``` - - - ::: -### ONNX Supported Models +### Configuration {#configuration-onnx} -| Model | Nvidia GPU | AMD GPU | Notes | -| ------------------------------------ | ---------- | ------- | --------------------------------------------------- | -| [YOLOv9](#yolo-v3-v4-v7-v9-2) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | -| [RF-DETR](#rf-detr) | ✅ | ⚠️ | Supports CUDA Graphs for optimal Nvidia performance | -| [YOLO-NAS](#yolo-nas-1) | ⚠️ | ⚠️ | Not supported by CUDA Graphs | -| [YOLOX](#yolox-1) | ✅ | ✅ | Supports CUDA Graphs for optimal Nvidia performance | -| [D-FINE / DEIMv2](#d-fine--deimv2-1) | ⚠️ | ❌ | Not supported by CUDA Graphs | + -There is no default model provided, the following formats are supported: - -#### YOLO-NAS - -[YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) models are supported, but not included by default. See [the models section](#downloading-yolo-nas-model) for more information on downloading the YOLO-NAS model for use in Frigate. - -
- YOLO-NAS Setup & Config - -:::warning - -If you are using a Frigate+ YOLO-NAS model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. - -::: - -After placing the downloaded onnx model in your config folder, use the following configuration: - - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------------- | -| **Object Detection Model Type** | `yolonas` | -| **Object detection model input width** | `320` (should match whatever was set in notebook) | -| **Object detection model input height** | `320` (should match whatever was set in notebook) | -| **Model Input Pixel Color Format** | `bgr` | -| **Model Input Tensor Shape** | `nchw` | -| **Custom object detector model path** | `/config/yolo_nas_s.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: yolonas - width: 320 # <--- should match whatever was set in notebook - height: 320 # <--- should match whatever was set in notebook - input_pixel_format: bgr - input_tensor: nchw - path: /config/yolo_nas_s.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -
- -#### YOLO (v3, v4, v7, v9) - -YOLOv3, YOLOv4, YOLOv7, and [YOLOv9](https://github.com/WongKinYiu/yolov9) models are supported, but not included by default. - -:::tip - -The YOLO detector has been designed to support YOLOv3, YOLOv4, YOLOv7, and YOLOv9 models, but may support other YOLO model architectures as well. See [the models section](#downloading-yolo-models) for more information on downloading YOLO models for use in Frigate. - -::: - -
- YOLOv Setup & Config - -:::warning - -If you are using a Frigate+ model, you should not define any of the below `model` parameters in your config except for `path`. See [the Frigate+ model docs](/plus/first_model#step-3-set-your-model-id-in-the-config) for more information on setting up your model. - -::: - -After placing the downloaded onnx model in your config folder, use the following configuration: - - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | -------------------------------------------------------- | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` (should match the imgsize set during model export) | -| **Object detection model input height** | `320` (should match the imgsize set during model export) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/yolo.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: yolo-generic - width: 320 # <--- should match the imgsize set during model export - height: 320 # <--- should match the imgsize set during model export - input_tensor: nchw - input_dtype: float - path: /config/model_cache/yolo.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -
- -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -#### YOLOx - -[YOLOx](https://github.com/Megvii-BaseDetection/YOLOX) models are supported, but not included by default. See [the models section](#downloading-yolo-models) for more information on downloading the YOLOx model for use in Frigate. - -
- YOLOx Setup & Config - -After placing the downloaded onnx model in your config folder, use the following configuration: - - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | -------------------------------------------------------- | -| **Object Detection Model Type** | `yolox` | -| **Object detection model input width** | `416` (should match the imgsize set during model export) | -| **Object detection model input height** | `416` (should match the imgsize set during model export) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float_denorm` | -| **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: yolox - width: 416 # <--- should match the imgsize set during model export - height: 416 # <--- should match the imgsize set during model export - input_tensor: nchw - input_dtype: float_denorm - path: /config/model_cache/yolox_tiny.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. - -
- -#### RF-DETR - -[RF-DETR](https://github.com/roboflow/rf-detr) is a DETR based model. The ONNX exported models are supported, but not included by default. See [the models section](#downloading-rf-detr-model) for more information on downloading the RF-DETR model for use in Frigate. - -
- RF-DETR Setup & Config - -After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration: - - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| --------------------------------------- | --------------------------------- | -| **Object Detection Model Type** | `rfdetr` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` | - - - - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: rfdetr - width: 320 - height: 320 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/rfdetr.onnx -``` - - - - -
- -#### D-FINE / DEIMv2 - -[D-FINE](https://github.com/Peterande/D-FINE) and [DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) are DETR based models that share the same ONNX input/output format. The ONNX exported models are supported, but not included by default. See the models section for downloading [D-FINE](#downloading-d-fine-model) or [DEIMv2](#downloading-deimv2-model) for use in Frigate. - -
- D-FINE Setup & Config - -After placing the downloaded onnx model in your `config/model_cache` folder, use the following configuration: - - - - -Navigate to and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------- | -| **Object Detection Model Type** | `dfine` | -| **Object detection model input width** | `640` | -| **Object detection model input height** | `640` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: dfine - width: 640 - height: 640 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/dfine_m_obj2coco.onnx - labelmap_path: /labelmap/coco-80.txt -``` - - - - -
- -
- DEIMv2 Setup & Config - -After placing the downloaded onnx model in your `config/model_cache` folder, you can use the following configuration: - -```yaml -detectors: - onnx: - type: onnx - -model: - model_type: dfine - width: 640 - height: 640 - input_tensor: nchw - input_dtype: float - path: /config/model_cache/deimv2_hgnetv2_n.onnx - labelmap_path: /labelmap/coco-80.txt -``` - -
- -Note that the labelmap uses a subset of the complete COCO label set that has only 80 objects. +--- ## CPU Detector (not recommended) @@ -1325,29 +475,9 @@ The number of threads used by the interpreter can be specified using the `"num_t 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`. - - +### Configuration {#configuration-cpu} -Navigate to 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). - - - - -```yaml -detectors: - cpu1: - type: cpu - num_threads: 3 - cpu2: - type: cpu - num_threads: 3 - -model: - path: "/custom_model.tflite" -``` - - - + When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance. @@ -1355,30 +485,15 @@ When using CPU detectors, you can add one CPU detector per camera. Adding more d The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking. -### Setup +### Setup {#setup-deepstack} To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation. To integrate CodeProject.AI into Frigate, configure the detector as follows: - - +### Configuration {#configuration-deepstack} -Navigate to 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://:/v1/vision/detection`). - - - - -```yaml -detectors: - deepstack: - api_url: http://:/v1/vision/detection - type: deepstack - api_timeout: 0.1 # seconds -``` - - - + Replace `` and `` with the IP address and port of your CodeProject.AI server. @@ -1394,257 +509,9 @@ See the [installation docs](../frigate/installation.md#memryx-mx3) for informati To configure a MemryX detector, simply set the `type` attribute to `memryx` and follow the configuration guide below. -### Configuration +### Configuration {#configuration-memryx} -To configure the MemryX detector, use the following example configuration: - -#### Single PCIe MemryX MX3 - - - - -Navigate to and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 -``` - - - - -#### Multiple PCIe MemryX MX3 Modules - - - - -Navigate to and select **MemryX** from the detector type dropdown and click **Add** to add multiple detectors, specifying `PCIe:0`, `PCIe:1`, `PCIe:2`, etc. as the device for each. - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 - - memx1: - type: memryx - device: PCIe:1 - - memx2: - type: memryx - device: PCIe:2 -``` - - - - -### Supported Models - -MemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`. - -#### YOLO-NAS - -The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded from the [Models Section](#downloading-yolo-nas-model) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage). - -**Note:** The default model for the MemryX detector is YOLO-NAS 320x320. - -The input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**. - -- The default size of **320x320** is optimized for lower CPU usage and faster inference times. - -##### Configuration - -Below is the recommended configuration for using the **YOLO-NAS** (small) model with the MemryX detector: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------------- | -| **Object Detection Model Type** | `yolonas` | -| **Object detection model input width** | `320` (can be set to `640` for higher resolution) | -| **Object detection model input height** | `320` (can be set to `640` for higher resolution) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 - -model: - model_type: yolonas - width: 320 # (Can be set to 640 for higher resolution) - height: 320 # (Can be set to 640 for higher resolution) - input_tensor: nchw - input_dtype: float - labelmap_path: /labelmap/coco-80.txt - # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. - # path: /config/yolonas.zip - # The .zip file must contain: - # ├── yolonas.dfp (a file ending with .dfp) - # └── yolonas_post.onnx (optional; only if the model includes a cropped post-processing network) -``` - - - - -#### YOLOv9 - -The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) like in the [Models Section](#yolov9-1) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage). - -##### Configuration - -Below is the recommended configuration for using the **YOLOv9** (small) model with the MemryX detector: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------------- | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` (can be set to `640` for higher resolution) | -| **Object detection model input height** | `320` (can be set to `640` for higher resolution) | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 - -model: - model_type: yolo-generic - width: 320 # (Can be set to 640 for higher resolution) - height: 320 # (Can be set to 640 for higher resolution) - input_tensor: nchw - input_dtype: float - labelmap_path: /labelmap/coco-80.txt - # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. - # path: /config/yolov9.zip - # The .zip file must contain: - # ├── yolov9.dfp (a file ending with .dfp) -``` - - - - -#### YOLOX - -The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP. - -##### Configuration - -Below is the recommended configuration for using the **YOLOX** (small) model with the MemryX detector: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ----------------------- | -| **Object Detection Model Type** | `yolox` | -| **Object detection model input width** | `640` | -| **Object detection model input height** | `640` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float_denorm` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 - -model: - model_type: yolox - width: 640 - height: 640 - input_tensor: nchw - input_dtype: float_denorm - labelmap_path: /labelmap/coco-80.txt - # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. - # path: /config/yolox.zip - # The .zip file must contain: - # ├── yolox.dfp (a file ending with .dfp) -``` - - - - -#### SSDLite MobileNet v2 - -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. - -##### Configuration - -Below is the recommended configuration for using the **SSDLite MobileNet v2** model with the MemryX detector: - - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ----------------------- | -| **Object Detection Model Type** | `ssd` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input D Type** | `float` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - memx0: - type: memryx - device: PCIe:0 - -model: - model_type: ssd - width: 320 - height: 320 - input_tensor: nchw - input_dtype: float - labelmap_path: /labelmap/coco-80.txt - # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model. - # path: /config/ssdlite_mobilenet.zip - # The .zip file must contain: - # ├── ssdlite_mobilenet.dfp (a file ending with .dfp) - # └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network) -``` - - - + #### Using a Custom Model @@ -1765,40 +632,7 @@ The TensorRT detector uses `.trt` model files that are located in `/config/model Use the config below to work with generated TRT models: - - - -Navigate to 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: - -| Field | Value | -| ---------------------------------------- | ------------------------------------------------------------ | -| **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | -| **Model Input Tensor Shape** | `nchw` | -| **Model Input Pixel Color Format** | `rgb` | -| **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | -| **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) | - - - - -```yaml -detectors: - tensorrt: - type: tensorrt - device: 0 #This is the default, select the first GPU - -model: - path: /config/model_cache/tensorrt/yolov7-320.trt - labelmap_path: /labelmap/coco-80.txt - input_tensor: nchw - input_pixel_format: rgb - width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416 - height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416 -``` - - - + ## Synaptics @@ -1812,47 +646,11 @@ This implementation is based on sdk `v1.5.0`. See the [installation docs](../frigate/installation.md#synaptics) for information on configuring the SL-series NPU hardware. -### Configuration +### Configuration {#configuration-synaptics} When configuring the Synap detector, you have to specify the model: a local **path**. -#### SSD Mobilenet - -A synap model is provided in the container at /mobilenet.synap and is used by this detector type by default. The model comes from [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80). - -Use the model configuration shown below when using the synaptics detector with the default synap model: - - - - -Navigate to and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ---------------------------- | -| **Custom object detector model path** | `/synaptics/mobilenet.synap` | -| **Object detection model input width** | `224` | -| **Object detection model input height** | `224` | -| **Model Input Tensor Shape** | `nhwc` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: # required - synap_npu: # required - type: synaptics # required - -model: # required - path: /synaptics/mobilenet.synap # required - width: 224 # required - height: 224 # required - input_tensor: nhwc # default value (optional. If you change the model, it is required) - labelmap_path: /labelmap/coco-80.txt # required -``` - - - + ## Rockchip platform @@ -1876,14 +674,6 @@ If no custom model is provided, the RKNN detector downloads a default model from When using many cameras one detector may not be enough to keep up. Multiple detectors can be defined assuming NPU resources are available. An example configuration would be: - - - -Navigate to and select **RKNN** from the detector type dropdown and click **Add** to add multiple detectors, each with `num_cores` set to `0` for automatic selection. - - - - ```yaml detectors: rknn_0: @@ -1894,9 +684,6 @@ detectors: num_cores: 0 ``` - - - ::: ### Prerequisites @@ -1918,27 +705,6 @@ $ cat /sys/kernel/debug/rknpu/load This `config.yml` shows all relevant options to configure the detector and explains them. All values shown are the default values (except for two). Lines that are required at least to use the detector are labeled as required, all other lines are optional. - - - -Navigate to and select **RKNN** from the detector type dropdown and click **Add**. Set `num_cores` to `0` for automatic selection (increase for better performance on multicore NPUs, e.g., set to `3` on rk3588). - - - - -```yaml -detectors: # required - rknn: # required - type: rknn # required - # number of NPU cores to use - # 0 means choose automatically - # increase for better performance if you have a multicore NPU e.g. set to 3 on rk3588 - num_cores: 0 -``` - - - - The inference time was determined on a rk3588 with 3 NPU cores. | Model | Size in mb | Inference time in ms | @@ -1953,136 +719,13 @@ The inference time was determined on a rk3588 with 3 NPU cores. - 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. - You 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. -#### YOLO-NAS - - - - -Navigate to and, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ----------------------------------------------------------------------- | -| **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) | -| **Object Detection Model Type** | `yolonas` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Pixel Color Format** | `bgr` | -| **Model Input Tensor Shape** | `nhwc` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -model: # required - # name of model (will be automatically downloaded) or path to your own .rknn model file - # possible values are: - # - deci-fp16-yolonas_s - # - deci-fp16-yolonas_m - # - deci-fp16-yolonas_l - # your yolonas_model.rknn - path: deci-fp16-yolonas_s - model_type: yolonas - width: 320 - height: 320 - input_pixel_format: bgr - input_tensor: nhwc - labelmap_path: /labelmap/coco-80.txt -``` - - - - -:::warning - -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 - -::: - -#### YOLO (v9) - - - - -Navigate to and, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | -------------------------------------------------- | -| **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input Tensor Shape** | `nhwc` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -model: # required - # name of model (will be automatically downloaded) or path to your own .rknn model file - # possible values are: - # - frigate-fp16-yolov9-t - # - frigate-fp16-yolov9-s - # - frigate-fp16-yolov9-m - # - frigate-fp16-yolov9-c - # - frigate-fp16-yolov9-e - # your yolo_model.rknn - path: frigate-fp16-yolov9-t - model_type: yolo-generic - width: 320 - height: 320 - input_tensor: nhwc - labelmap_path: /labelmap/coco-80.txt -``` - - - - -#### YOLOx - - - - -Navigate to and, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ---------------------------------------------- | -| **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) | -| **Object Detection Model Type** | `yolox` | -| **Object detection model input width** | `416` | -| **Object detection model input height** | `416` | -| **Model Input Tensor Shape** | `nhwc` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -model: # required - # name of model (will be automatically downloaded) or path to your own .rknn model file - # possible values are: - # - rock-i8-yolox_nano - # - rock-i8-yolox_tiny - # - rock-fp16-yolox_nano - # - rock-fp16-yolox_tiny - # your yolox_model.rknn - path: rock-i8-yolox_nano - model_type: yolox - width: 416 - height: 416 - input_tensor: nhwc - labelmap_path: /labelmap/coco-80.txt -``` - - - + ### Converting your own onnx model to rknn format To convert a onnx model to the rknn format using the [rknn-toolkit2](https://github.com/airockchip/rknn-toolkit2/) you have to: -- Place one ore more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges). +- Place one or more models in onnx format in the directory `config/model_cache/rknn_cache/onnx` on your docker host (this might require `sudo` privileges). - Save the configuration file under `config/conv2rknn.yaml` (see below for details). - Run `docker exec python3 /opt/conv2rknn.py`. If the conversion was successful, the rknn models will be placed in `config/model_cache/rknn_cache`. @@ -2100,13 +743,13 @@ config: quant_img_RGB2BGR: true ``` -Explanation of the paramters: +Explanation of the parameters: - `soc`: A list of all SoCs you want to build the rknn model for. If you don't specify this parameter, the script tries to find out your SoC and builds the rknn model for this one. - `quantization`: true: 8 bit integer (i8) quantization, false: 16 bit float (fp16). Default: false. - `output_name`: The output name of the model. The following variables are available: - `quant`: "i8" or "fp16" depending on the config - - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is calles "my_model.onnx") + - `input_basename`: the basename of the input model (e.g. "my_model" if the input model is called "my_model.onnx") - `soc`: the SoC this model was build for (e.g. "rk3588") - `tk_version`: Version of `rknn-toolkit2` (e.g. "2.3.0") - **example**: Specifying `output_name = "frigate-{quant}-{input_basename}-{soc}-v{tk_version}"` could result in a model called `frigate-i8-my_model-rk3588-v2.3.0.rknn`. @@ -2116,7 +759,7 @@ Explanation of the paramters: DeGirum is a detector that can use any type of hardware listed on [their website](https://hub.degirum.com). DeGirum can be used with local hardware through a DeGirum AI Server, or through the use of `@local`. You can also connect directly to DeGirum's AI Hub to run inferences. **Please Note:** This detector _cannot_ be used for commercial purposes. -### Configuration +### Configuration {#configuration-degirum} #### AI Server Inference @@ -2135,24 +778,7 @@ All supported hardware will automatically be found on your AI server host as lon Once completed, configure the detector as follows: - - - -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to your AI server (e.g., service name, container name, or `host:port`), the zoo to `degirum/public`, and provide your authentication token if needed. - - - - -```yaml -degirum_detector: - type: degirum - location: degirum # Set to service name (degirum_detector), container_name (degirum), or a host:port (192.168.29.4:8778) - zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. If you aren't pulling a model from the AI Hub, leave this and 'token' blank. - token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server -``` - - - + Setting up a model in the `config.yml` is similar to setting up an AI server. You can set it to: @@ -2178,24 +804,7 @@ It is also possible to eliminate the need for an AI server and run the hardware 2. To double check the runtime is detected by the DeGirum detector, make sure the `degirum sys-info` command properly shows whatever runtimes you mean to install. 3. Create a DeGirum detector in your configuration. - - - -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@local`, the zoo to `degirum/public`, and provide your authentication token. - - - - -```yaml -degirum_detector: - type: degirum - location: "@local" # For accessing AI Hub devices and models - zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. - token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the [AI Hub](https://hub.degirum.com). This can be left blank if you're pulling a model from the public zoo and running inferences on your local hardware using @local or a local DeGirum AI Server -``` - - - + Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. @@ -2215,24 +824,7 @@ If you do not possess whatever hardware you want to run, there's also the option 2. Get an access token. 3. Create a DeGirum detector in your configuration. - - - -Navigate to and select **DeGirum** from the detector type dropdown and click **Add**. Set the location to `@cloud`, the zoo to `degirum/public`, and provide your authentication token. - - - - -```yaml -degirum_detector: - type: degirum - location: "@cloud" # For accessing AI Hub devices and models - zoo: degirum/public # DeGirum's public model zoo. Zoo name should be in format "workspace/zoo_name". degirum/public is available to everyone, so feel free to use it if you don't know where to start. - token: dg_example_token # For authentication with the AI Hub. Get this token through the "tokens" section on the main page of the (AI Hub)[https://hub.degirum.com). -``` - - - + Once `degirum_detector` is setup, you can choose a model through 'model' section in the `config.yml` file. @@ -2261,202 +853,8 @@ The AXEngine detector downloads its default model from HuggingFace on first star ::: -### Configuration +### Configuration {#configuration-axengine} When configuring the AXEngine detector, you have to specify the model name. -#### yolov9 - -A yolov9 model is provided in the container at `/axmodels` and is used by this detector type by default. - -Use the model configuration shown below when using the axengine detector with the default axmodel: - - - - -Navigate to and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure: - -| Field | Value | -| ---------------------------------------- | ----------------------- | -| **Custom object detector model path** | `frigate-yolov9-tiny` | -| **Object Detection Model Type** | `yolo-generic` | -| **Object detection model input width** | `320` | -| **Object detection model input height** | `320` | -| **Model Input D Type** | `int` | -| **Model Input Pixel Color Format** | `bgr` | -| **Label map for custom object detector** | `/labelmap/coco-80.txt` | - - - - -```yaml -detectors: - axengine: - type: axengine - -model: - path: frigate-yolov9-tiny - model_type: yolo-generic - width: 320 - height: 320 - input_dtype: int - input_pixel_format: bgr - labelmap_path: /labelmap/coco-80.txt -``` - - - - -# Models - -Some model types are not included in Frigate by default. - -## Downloading Models - -Here are some tips for getting different model types - -### Downloading D-FINE Model - -D-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. - -```sh -docker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF' -FROM python:3.11 AS build -RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ -WORKDIR /dfine -RUN git clone https://github.com/Peterande/D-FINE.git . -RUN uv pip install --system -r requirements.txt -RUN uv pip install --system onnx onnxruntime onnxsim onnxscript -# Create output directory and download checkpoint -RUN mkdir -p output -ARG MODEL_SIZE -RUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth -# Modify line 58 of export_onnx.py to change batch size to 1 -RUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py -RUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth -FROM scratch -ARG MODEL_SIZE -COPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx -EOF -``` - -### Downloading DEIMv2 Model - -[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: - -- **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n` -- **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x` - -Set `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`). - -```sh -docker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF' -FROM python:3.11-slim AS build -RUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/ -WORKDIR /deimv2 -RUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git . -# Install CPU-only PyTorch first to avoid pulling CUDA variant -RUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu -RUN uv pip install --no-cache --system -r requirements.txt -RUN uv pip install --no-cache --system onnx safetensors huggingface_hub -RUN mkdir -p output -ARG BACKBONE -ARG MODEL_SIZE -# Download from Hugging Face and convert safetensors to pth -RUN python3 -c "\ -from huggingface_hub import hf_hub_download; \ -from safetensors.torch import load_file; \ -import torch; \ -backbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \ -size = '${MODEL_SIZE}'.upper(); \ -st = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \ -torch.save({'model': st}, 'output/deimv2.pth')" -RUN sed -i "s/data = torch.rand(2/data = torch.rand(1/" tools/deployment/export_onnx.py -# HuggingFace safetensors omits frozen constants that the model constructor initializes -RUN sed -i "s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/" tools/deployment/export_onnx.py -RUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth -FROM scratch -ARG BACKBONE -ARG MODEL_SIZE -COPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx -EOF -``` - -### Downloading RF-DETR Model - -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. - -```sh -docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF' -FROM python:3.12 AS build -RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ -WORKDIR /rfdetr -RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript -ARG MODEL_SIZE -RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)" -FROM scratch -ARG MODEL_SIZE -COPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx -EOF -``` - -### Downloading YOLO-NAS Model - -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). - -:::warning - -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 - -::: - -The 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. - -### Downloading YOLO Models - -#### YOLOx - -YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime). - -#### YOLOv3, YOLOv4, and YOLOv7 - -To export as ONNX: - -```sh -git clone https://github.com/NateMeyer/tensorrt_demos -cd tensorrt_demos/yolo -./download_yolo.sh -python3 yolo_to_onnx.py -m yolov7-320 -``` - -#### YOLOv9 for Google Coral Support - -[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. - -#### YOLOv9 for other detectors - -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`). - -```sh -docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF' -FROM python:3.11 AS build -RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/* -COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/ -WORKDIR /yolov9 -ADD https://github.com/WongKinYiu/yolov9.git . -RUN uv pip install --system -r requirements.txt -RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript -ARG MODEL_SIZE -ARG IMG_SIZE -ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt -RUN 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 -RUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx -FROM scratch -ARG MODEL_SIZE -ARG IMG_SIZE -COPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx -EOF -``` + diff --git a/docs/docs/configuration/object_filters.md b/docs/docs/configuration/object_filters.md index 8a492960df..b2e7fca319 100644 --- a/docs/docs/configuration/object_filters.md +++ b/docs/docs/configuration/object_filters.md @@ -7,7 +7,7 @@ import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; -There are several types of object filters that can be used to reduce false positive rates. +There are several types of object filters that can be used to reduce [false positive](/frigate/glossary#false-positive) rates. ## Object Scores @@ -26,9 +26,9 @@ In frame 2, the score is below the `min_score` value, so Frigate ignores it and The **top score** is the highest computed score the tracked object has ever reached during its lifetime. Because the computed score rises and falls as new frames come in, the top score can be thought of as the peak confidence Frigate had in the object. In Frigate's UI (such as the Tracking Details pane in Explore), you may see all three values: -- **Score** — the raw detector score for that single frame. -- **Computed Score** — the median of the most recent score history at that moment. This is the value compared against `threshold`. -- **Top Score** — the highest computed score reached so far for the tracked object. +- **Score**: the raw detector score for that single frame. +- **Computed Score**: the median of the most recent score history at that moment. This is the value compared against `threshold`. +- **Top Score**: the highest computed score reached so far for the tracked object. ### Minimum Score @@ -36,7 +36,7 @@ Any detection below `min_score` will be immediately thrown out and never tracked ### Threshold -`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create an tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough. +`threshold` is used to determine that the object is a true positive. Once an object is detected with a score >= `threshold` object is considered a true positive. If `threshold` is too low then some higher scoring false positives may create a tracked object. If `threshold` is too high then true positive tracked objects may be missed due to the object never scoring high enough. ## Configuring Object Scores @@ -144,8 +144,8 @@ cameras: ### Zones -[Required zones](/configuration/zones.md) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone. +[Required zones](/configuration/zones.md#restricting-alerts-and-detections-to-specific-zones) can be a great tool to reduce false positives that may be detected in the sky or other areas that are not of interest. The required zones will only create tracked objects for objects that enter the zone. ### Object Masks -[Object Filter Masks](/configuration/masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. Object filter masks can be configured in . +[Object Filter Masks](/configuration/masks#object-filter-masks) are a last resort but can be useful when false positives are in the relatively same place but can not be filtered due to their size or shape. Object filter masks can be configured in . diff --git a/docs/docs/configuration/profiles.md b/docs/docs/configuration/profiles.md index 4d93168f81..3eab2bd131 100644 --- a/docs/docs/configuration/profiles.md +++ b/docs/docs/configuration/profiles.md @@ -14,13 +14,13 @@ Profiles allow you to define named sets of camera configuration overrides that c Profiles operate as a two-level system: 1. **Profile definitions** are declared at the top level of your config under `profiles`. Each definition has a machine name (the key) and a `friendly_name` for display in the UI. -2. **Camera profile overrides** are declared under each camera's `profiles` section, keyed by the profile name. Only the settings you want to change need to be specified — everything else is inherited from the camera's base configuration. +2. **Camera profile overrides** are declared under each camera's `profiles` section, keyed by the profile name. Only the settings you want to change need to be specified. Everything else is inherited from the camera's base configuration. When a profile is activated, Frigate merges each camera's profile overrides on top of its base config. When the profile is deactivated, all cameras revert to their original settings. Only one profile can be active at a time. :::info -Profile changes are applied in-memory and take effect immediately — no restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file). +Profile changes are applied in-memory and take effect immediately. No restart is required. The active profile is persisted across Frigate restarts (stored in the `/config/.profiles` file). ::: @@ -33,10 +33,10 @@ The easiest way to define profiles is to use the Frigate UI. Profiles can also b -1. **Create a profile** — Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID). -2. **Configure overrides** — Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides — fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides. -3. **Activate a profile** — Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers. -4. **Delete a profile** — Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it. +1. **Create a profile**: Navigate to . Click the **Add Profile** button, enter a name (and optionally a profile ID). +2. **Configure overrides**: Navigate to a camera configuration section (e.g. Motion detection, Record, Notifications). In the top right, two buttons will appear - choose a camera and a profile from the profile selector to edit overrides for that camera and section. Only the fields you change will be stored as overrides. Fields that require a restart are hidden since profiles are applied at runtime. You can click the **Remove Profile Override** button to clear overrides. +3. **Activate a profile**: Use the **Profiles** option in Frigate's main menu to choose a profile. Alternatively, in Settings, navigate to , then choose a profile in the Active Profile dropdown to activate it. The active profile is also shown in the status bar at the bottom of the screen on desktop browsers. +4. **Delete a profile**: Navigate to , then click the trash icon for a profile. This removes the profile definition and all camera overrides associated with it. @@ -235,3 +235,9 @@ When you delete a base zone or mask in the Frigate UI, any profile overrides for ### Why are some settings missing when I configure a profile override? Fields that require a Frigate restart to take effect cannot be overridden by profiles, since profiles are applied at runtime without restarting. Those fields are hidden when editing a profile override and can only be changed on the base configuration. + +### Can I schedule profiles to be enabled or disabled at certain times? + +Not within Frigate itself. Frigate is an NVR, not an automation platform, so it intentionally does not include a scheduler for activating profiles. Instead, activate profiles from an automation platform that already handles time- and event-based triggers well, such as [Home Assistant](https://www.home-assistant.io/) or [Node-RED](https://nodered.org/). These integrate with Frigate and give you far more robust and flexible scheduling than a built-in scheduler could. + +If you prefer something lightweight, a simple script driven by a cron job that toggles profiles on a schedule works too. diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md index 4ce55e2ee1..87a24d3d60 100644 --- a/docs/docs/configuration/record.md +++ b/docs/docs/configuration/record.md @@ -170,9 +170,9 @@ record: The `pre_capture` and `post_capture` values define the **time window** around a review item, but only recording segments that also match the configured **retention mode** are actually kept on disk. -- **`mode: all`** — Retains every segment within the capture window, regardless of whether motion was detected. -- **`mode: motion`** (default) — Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range. -- **`mode: active_objects`** — Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded. +- **`mode: all`**: Retains every segment within the capture window, regardless of whether motion was detected. +- **`mode: motion`** (default): Only retains segments within the capture window that contain motion. This includes segments with active tracked objects, since object motion implies motion. Segments without any motion are discarded even if they fall within the pre/post capture range. +- **`mode: active_objects`**: Only retains segments within the capture window where tracked objects were actively moving. Segments with general motion but no active objects are discarded. This means that with the default `motion` mode, you may see less footage than the configured pre/post capture duration if parts of the capture window had no motion. @@ -197,11 +197,7 @@ Because recording segments are written in 10 second chunks, pre-capture timing d ### Where to view pre/post capture footage -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. +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. ## Configuring Recording Retention @@ -355,3 +351,63 @@ Setting `verbose: true` writes a detailed report of every orphaned file and data This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended. ::: + +## Understanding storage usage + +The storage usage Frigate reports will not exactly match what the operating system reports with `df` or `du`. This is expected, not a bug. The sections below explain how Frigate derives its storage figures and why they differ from the disk's own accounting. + +### How Frigate measures recording usage + +The **Recordings** value on the Storage Metrics page (), 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. diff --git a/docs/docs/configuration/restream.md b/docs/docs/configuration/restream.md index d488c54104..1ae96b64cc 100644 --- a/docs/docs/configuration/restream.md +++ b/docs/docs/configuration/restream.md @@ -11,7 +11,7 @@ import NavPath from "@site/src/components/NavPath"; Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://:8554/`. Port 8554 must be open. [This allows you to use a video feed for detection in Frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate. -Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.13) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration) for more advanced configurations and features. +Frigate uses [go2rtc](https://github.com/AlexxIT/go2rtc/tree/v1.9.14) to provide its restream and MSE/WebRTC capabilities. The go2rtc config is hosted at the `go2rtc` in the config, see [go2rtc docs](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration) for more advanced configurations and features. :::note @@ -61,7 +61,7 @@ Configure the go2rtc stream and point the camera inputs at the local restream. -Navigate to and add stream entries for each camera. Then navigate to for each camera and set the input paths to use the local restream URL (`rtsp://127.0.0.1:8554/`). +Navigate to and add stream entries for each camera. Then navigate to for each camera. For each input, choose **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL (`rtsp://127.0.0.1:8554/`) and the `preset-rtsp-restream` input args for that input automatically. (Choose **Manual input path** instead to type a URL directly.) @@ -111,7 +111,7 @@ Two connections are made to the camera. One for the sub stream, one for the rest -Navigate to and add stream entries for each camera and its sub stream. Then navigate to for each camera and configure separate inputs for the main and sub streams using the local restream URLs. +Navigate to and add stream entries for each camera and its sub stream. Then navigate to for each camera and add separate inputs for the main and sub streams. Set each input's source to **Restream (go2rtc)** and pick the matching stream from the dropdown. Frigate uses the local restream URL and the `preset-rtsp-restream` input args for that input automatically. @@ -236,7 +236,7 @@ Enabling arbitrary exec sources allows execution of arbitrary commands through g ## Advanced Restream Configurations -The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below: +The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-exec) source in go2rtc can be used for custom ffmpeg commands and other applications. An example is below: :::warning diff --git a/docs/docs/configuration/review.md b/docs/docs/configuration/review.md index 199212602d..e9ba4df254 100644 --- a/docs/docs/configuration/review.md +++ b/docs/docs/configuration/review.md @@ -23,7 +23,7 @@ In 0.14 and later, all of that is bundled into a single review item which starts ## Alerts and Detections -Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring required zones for them. +Not every segment of video captured by Frigate may be of the same level of interest to you. Video of people who enter your property may be a different priority than those walking by on the sidewalk. For this reason, Frigate categorizes review items as _alerts_ and _detections_. By default, all person and car objects are considered alerts. You can refine categorization of your review items by configuring [required zones](/configuration/zones#restricting-alerts-and-detections-to-specific-zones) for them. :::note diff --git a/docs/docs/configuration/semantic_search.md b/docs/docs/configuration/semantic_search.md index dc7c3a4d03..c58b634fd8 100644 --- a/docs/docs/configuration/semantic_search.md +++ b/docs/docs/configuration/semantic_search.md @@ -7,7 +7,7 @@ import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; -Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_ — numerical vector representations — for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results. +Semantic Search in Frigate allows you to find tracked objects within your review items using either the image itself, a user-defined text description, or an automatically generated one. This feature works by creating _embeddings_, numerical vector representations, for both the images and text descriptions of your tracked objects. By comparing these embeddings, Frigate assesses their similarities to deliver relevant search results. Frigate uses models from [Jina AI](https://huggingface.co/jinaai) to create and save embeddings to Frigate's database. All of this runs locally. @@ -222,11 +222,11 @@ See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_ ## Usage and Best Practices -For tips on getting the best results from Semantic Search — choosing between thumbnail and description search, phrasing queries effectively, and combining search with the other Explore filters — see [Usage and best practices](/usage/explore#usage-and-best-practices) in the Usage docs. +For tips on getting the best results from Semantic Search (choosing between thumbnail and description search, phrasing queries effectively, and combining search with the other Explore filters), see [Usage and best practices](/usage/explore#usage-and-best-practices) in the Usage docs. ## Triggers -Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes a specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab. +Triggers utilize Semantic Search to automate actions when a tracked object matches a specified image or description. Triggers can be configured so that Frigate executes specific actions when a tracked object's image or description matches a predefined image or text, based on a similarity threshold. Triggers are managed per camera and can be configured via the Frigate UI in the Settings page under the Triggers tab. :::note diff --git a/docs/docs/configuration/snapshots.md b/docs/docs/configuration/snapshots.md index 675e68a9ca..b2aba9463f 100644 --- a/docs/docs/configuration/snapshots.md +++ b/docs/docs/configuration/snapshots.md @@ -7,13 +7,17 @@ import ConfigTabs from "@site/src/components/ConfigTabs"; import TabItem from "@theme/TabItem"; import NavPath from "@site/src/components/NavPath"; -Frigate can save a snapshot image to `/media/frigate/clips` for each object that is detected named as `--clean.webp`. They are also accessible [via the api](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx) +A snapshot is a single still image that captures a tracked object at its best moment: the clearest frame Frigate saw while following that object across the scene. Unlike a [recording](./record.md), which is continuous video, a snapshot is one representative image saved per tracked object once tracking ends. -Snapshots are accessible in the UI in the Explore pane. This allows for quick submission to the Frigate+ service. +When snapshots are enabled, Frigate saves one image to `/media/frigate/clips` for each tracked object, named `--clean.webp`. A clean image is always stored without any annotations (no timestamp, bounding boxes, or cropping) so you have an unmodified copy of the original frame. Annotations like bounding boxes and timestamps are applied on demand when a snapshot is requested [via the HTTP API](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx). See [Rendering](#rendering) below. -To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones) +A few things to keep in mind: -Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here. +- Snapshots are saved per tracked object, so a camera with no detected objects produces no snapshots even if recording is enabled. +- Snapshots and recordings are configured and retained independently. Enabling one does not enable the other. +- Snapshots are accessible in the UI in the Explore pane, which allows for quick submission to the Frigate+ service. +- To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones). +- Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here. ## Enabling Snapshots @@ -107,7 +111,6 @@ Navigate to . | Field | Description | | -------------------------------------------------- | ----------------------------------------------------------------------------------- | | **Snapshot retention > Default retention** | Number of days to retain snapshots (default: 10) | -| **Snapshot retention > Retention mode** | Retention mode: `all`, `motion`, or `active_objects` | | **Snapshot retention > Object retention > Person** | Per-object overrides for retention days (e.g., keep `person` snapshots for 15 days) | @@ -118,7 +121,6 @@ snapshots: enabled: True retain: default: 10 - mode: motion objects: person: 15 ``` @@ -130,7 +132,7 @@ snapshots: Frigate does not save every frame. It picks a single "best" frame for each tracked object based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. That best frame is written to disk once tracking ends. -MQTT snapshots are published more frequently — each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under the camera MQTT settings. +MQTT snapshots are published more frequently: each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under the camera MQTT settings. ## Rendering diff --git a/docs/docs/configuration/stationary_objects.md b/docs/docs/configuration/stationary_objects.md index 63d03374c8..40dda40e07 100644 --- a/docs/docs/configuration/stationary_objects.md +++ b/docs/docs/configuration/stationary_objects.md @@ -43,7 +43,7 @@ Let's look at an example use case: I want to record any cars that enter my drive One might simply think "Why not just run object detection any time there is motion around the driveway area and notify if the bounding box is in that zone?" -With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time. +With that approach, what video is related to the car that entered the driveway? Did it come from the left or right? Was it parked across the street for an hour before turning into the driveway? One approach is to just record 24/7 or for motion (on any changed pixels) and not attempt to do that at all. This is what most other NVRs do. Just don't even try to identify a start and end for that object since it's hard and you will be wrong some portion of the time. Couldn't you just look at when motion stopped and started? Motion for a video feed is nothing more than looking for pixels that are different than they were in previous frames. If the car entered the driveway while someone was mowing the grass, how would you know which motion was for the car and which was for the person when they mow along the driveway or street? What if another car was driving the other direction on the street? Or what if its a windy day and the bush by your mailbox is blowing around? @@ -61,4 +61,4 @@ Now you have to determine which of the bounding boxes in this frame should be ma Now let's assume that those other 3 cars were already being tracked as stationary objects, so the car driving down the street is a new 4th car. The object tracker knows we have had 3 cars and we now have 4. As the new car approaches the parked cars, the bounding boxes for all 4 cars is predicted based on the previous frames. The predicted boxes for the parked cars is pretty much a 100% overlap with the bounding boxes in the new frame. The parked cars are slam dunk matches to the tracking ids they had before and the only one left is the remaining bounding box which gets assigned to the new car. This results in a much lower error rate. Not perfect, but better. -The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and its no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future. +The most difficult scenario that causes IDs to be assigned incorrectly is when an object completely occludes another object. When a car drives in front of another car and it's no longer visible, a bounding box disappeared and it's a bit of a toss up when assigning the id since it's difficult to know which one is in front of the other. This happens for cars passing in front of other cars fairly often. It's something that we want to improve in the future. diff --git a/docs/docs/configuration/zones.md b/docs/docs/configuration/zones.md index 2cb3c8ebeb..2a26453203 100644 --- a/docs/docs/configuration/zones.md +++ b/docs/docs/configuration/zones.md @@ -18,7 +18,7 @@ Zones cannot have the same name as a camera. If desired, a single zone can inclu Zones can be toggled on or off without removing them from the configuration. Disabled zones are completely ignored at runtime - objects will not be tracked for zone presence, and zones will not appear in the debug view. This is useful for temporarily disabling a zone during certain seasons or times of day without modifying the configuration. -During testing, enable the Zones option for the Debug view of your camera (Settings --> Debug) so you can adjust as needed. The zone line will increase in thickness when any object enters the zone. +During testing, enable the Zones option for the [Debug view](/usage/live#the-single-camera-view) of your camera so you can adjust as needed. The zone line will increase in thickness when any object enters the zone. ## Creating a Zone @@ -61,7 +61,7 @@ Navigate to . | Field | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------- | -| **Alerts config > Required zones** | Zones that an object must enter to be considered an alert; leave empty to allow any zone. | +| **Alerts config > Required zones** | Set to `entire_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. | @@ -82,7 +82,7 @@ cameras: -You may also want to filter detections to only be created when an object enters a secondary area of interest. For example, to trigger alerts when an object enters the inner area of the yard but detections when an object enters the edge of the yard: +You may also want to filter detections to only be created when an object enters a secondary area of interest. For example, to trigger alerts when an object enters the inner area of the yard (an `inner_yard` zone) but detections when an object enters the edge of the yard (an `edge_yard` zone): @@ -91,8 +91,8 @@ Navigate to . | Field | Description | | -------------------------------------- | -------------------------------------------------------------------------------------------- | -| **Alerts config > Required zones** | Zones that an object must enter to be considered an alert; leave empty to allow any zone. | -| **Detections config > Required zones** | Zones that an object must enter to be considered a detection; leave empty to allow any zone. | +| **Alerts config > Required zones** | Set to `inner_yard` so an object must enter that zone to be considered an alert; leave empty to allow alerts anywhere in the frame. | +| **Detections config > Required zones** | Set to `edge_yard` so an object must enter that zone to be considered a detection; leave empty to allow detections anywhere in the frame. | @@ -121,7 +121,7 @@ cameras: ### Restricting snapshots to specific zones -To only save snapshots when an object enters a specific zone: +To only save snapshots when an object enters a specific zone, for example an `entire_yard` zone: diff --git a/docs/docs/frigate/camera_setup.md b/docs/docs/frigate/camera_setup.md index 4cb56dc508..0299a9aab8 100644 --- a/docs/docs/frigate/camera_setup.md +++ b/docs/docs/frigate/camera_setup.md @@ -5,7 +5,7 @@ title: Camera setup Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but less compatibility. Firefox 134+/136+/137+ (Windows/Mac/Linux & Android), Chrome 108+, Safari and Edge are the only browsers able to play H.265 and only support a limited number of H.265 profiles. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around. -- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher (10fps is the recommended maximum for most users) for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server. +- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The default frame rate of 5fps is correct for almost all cameras and rarely needs to be changed; see [Choosing a detect frame rate](#choosing-a-detect-frame-rate). Higher resolutions and frame rates will drive higher CPU usage on your server. - **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed in your camera's firmware to 15 fps. @@ -25,6 +25,44 @@ Larger resolutions **do** improve performance if the objects are very small in t ![Resolutions](/img/resolutions-min.jpg) +### Choosing a detect frame rate + +`detect.fps` controls how many times per second Frigate runs object detection. It does **not** need to match your camera's frame rate. The default of **5** is correct for the vast majority of cameras. + +:::warning + +Most users who raise `detect.fps` above the default don't need to. Increasing it consumes more CPU/GPU (detection load scales directly with the frame rate) while providing **no benefit to tracking** once objects are already being followed smoothly. Leave it at **5** unless you have a specific scene that fails the test below, and confirm any change actually helps in the [debug view](/usage/live#the-single-camera-view). + +::: + +#### Why 5 is enough for almost everyone + +Frigate follows an object by matching its bounding box from one detection frame to the next, which requires the object to be detected often enough while it is on screen. At 5 fps this is satisfied in normal scenes: an object crossing a yard, porch, driveway, or walkway is in view for several seconds and produces ~15 or more detections, which is more than enough for a reliable track and a good snapshot. This includes fast subjects such as a running person or a bolting pet, which on a wide-angle view remain on screen for several seconds. + +A higher rate helps only when an object crosses the **entire frame in less than two seconds**, which is determined by camera framing rather than object speed - for example, a camera aimed down a street at fast cross-traffic. In those scenes 5 fps may produce too few detections to hold a track. Cameras covering normal approaches and open areas are unaffected. + +#### Checking whether a higher rate is needed + +Estimate how long an object is visible as it crosses the area of interest, aiming for roughly 8–10 detections during the pass: + +> **`detect.fps` ≈ 10 ÷ (seconds the object is in view)** + +Most objects (people walking or running, pets, and vehicles in a yard, driveway, or walkway) stay in view for two seconds or more, so the default of 5 fps is correct. Slowly try raising it to 10 (the recommended maximum) in increments only when objects routinely cross the entire frame in about a second, such as a camera aimed at a street or sidewalk with fast cross-traffic. Objects that transit in under a second cannot be tracked reliably at any practical rate, so reposition the camera instead. + +:::tip + +If the formula calls for more than 10, the fix is **camera placement, not frame rate**. Angle the camera so objects move toward it rather than across the view, or aim it where traffic slows. A higher `detect.fps` increases CPU load proportionally without producing more detections of a too-brief object. + +::: + +#### Verify in the debug view + +Confirm any change in the Debug view or Debug Replay. Watch a typical object cross the scene: if its bounding box follows it smoothly while visible, the rate is sufficient. A box that jumps erratically, drops out, or splits one object into multiple events indicates the rate should be increased one step. + +#### Dedicated LPR cameras + +A dedicated license plate recognition camera is the most common reason to use something higher than 5 fps: the camera is highly zoomed, the plate is small, and it moves at full vehicle speed, so it transits the frame quickly. However, the same ceiling applies: above 10 fps is unnecessary, and **placement matters most**: aim LPR cameras where vehicles slow down, such as gates, driveways, and parking entrances. A tight view of a fast through-road will not likely read plates reliably at any frame rate. See [License Plate Recognition](/configuration/license_plate_recognition) for details. + ### Example Camera Configuration For the Dahua/Loryta 5442 camera, I use the following settings: diff --git a/docs/docs/frigate/glossary.md b/docs/docs/frigate/glossary.md index 5bfbfafa5a..d550015670 100644 --- a/docs/docs/frigate/glossary.md +++ b/docs/docs/frigate/glossary.md @@ -5,20 +5,40 @@ title: Glossary The glossary explains terms commonly used in Frigate's documentation. +## Alert + +The higher-priority of the two [review item](#review-item) severities, the other being a [detection](#detection). By default a review item is an alert when it involves a `person` or `car`; the qualifying [labels](#label) and [zones](#zone) can be configured. [See the review docs for more info](/configuration/review) + +## Attribute + +A property detected on an [object](#object) that exists alongside its [label](#label). Unlike a [sub label](#sub-label), an object can carry several attributes at once. Some attributes come directly from the object detection [model](#model) (for example `face`, `license_plate`, or delivery carrier logos such as `amazon`, `ups`, and `fedex`), while others come from a [custom object classification model](/configuration/custom_classification/object_classification) configured with the `attribute` type. Attributes are visible in the Tracked Object Details pane in Explore, in `frigate/events` MQTT messages, and through the HTTP API. + ## Bounding Box -A box returned from the object detection model that outlines an object in the frame. These have multiple colors depending on object type in the debug live view. +A box returned by the object detection [model](#model) that outlines a detected [object](#object) in the frame. In the [Debug view](/usage/live#the-single-camera-view), bounding boxes are colored by object [label](#label). ### Bounding Box Colors - At startup different colors will be assigned to each object label - A dark blue thin line indicates that object is not detected at this current point in time - A gray thin line indicates that object is detected as being stationary -- A thick line indicates that object is the subject of autotracking (when enabled). +- A thick line indicates that object is the subject of autotracking (when enabled) + +## Class + +The categories a classification [model](#model) is trained to distinguish between. Each class is a distinct visual category the model predicts, plus a `none` class for inputs that don't fit any category. For example, a custom object classification model for `person` objects might use the classes `delivery_person`, `resident`, and `none`. The predicted class is applied to the [object](#object) as either a [sub label](#sub-label) or an [attribute](#attribute), depending on the model's configuration. [See the object classification docs for more info](/configuration/custom_classification/object_classification) + +## Detection + +The lower-priority of the two [review item](#review-item) severities, the other being an [alert](#alert). By default, any review item that does not qualify as an alert is a detection; the qualifying [labels](#label) and [zones](#zone) can be configured. Despite the name, a detection is a category of review item, not the same as the object detection performed by the [model](#model). [See the review docs for more info](/configuration/review) ## False Positive -An incorrect detection of an object type. For example a dog being detected as a person, a chair being detected as a dog, etc. A person being detected in an area you want to ignore is not a false positive. +An incorrect result from the object detection [model](#model), where it assigns the wrong [label](#label) to something in the frame, for example a dog identified as a person, or a chair identified as a dog. A person correctly identified in an area you want to ignore is not a false positive. + +## Label + +The type assigned to a detected [object](#object) by the object detection [model](#model), drawn from the model's labelmap, for example `person`, `car`, or `dog`. Frigate tracks `person` by default; additional labels are tracked by adding them to the objects configuration. [See the available objects docs for the full list](/configuration/objects) ## Mask @@ -26,44 +46,56 @@ There are two types of masks in Frigate. [See the mask docs for more info](/conf ### Motion Mask -Motion masks prevent detection of [motion](#motion) in masked areas from triggering Frigate to run object detection, but do not prevent objects from being detected if object detection runs due to motion in nearby areas. For example: camera timestamps, skies, the tops of trees, etc. +A motion mask stops [motion](#motion) in the masked area from triggering object detection. It does not stop an object from being detected when object detection runs because of motion in a nearby area. Use motion masks for parts of the frame that change constantly but never contain objects you care about: camera timestamps, the sky, the tops of trees, and so on. ### Object Mask -Object filter masks drop any bounding boxes where the bottom center (overlap doesn't matter) is in the masked area. It forces them to be considered a [false positive](#false-positive) so that they are ignored. +An object filter mask drops any [bounding box](#bounding-box) whose bottom center falls inside the masked area (overlap elsewhere doesn't matter). The object is forced to be treated as a [false positive](#false-positive) and ignored. ## Min Score -The lowest score that an object can be detected with during tracking, any detection with a lower score will be assumed to be a false positive +The lowest score a detected object can have to be kept during tracking. Anything scoring below the minimum is assumed to be a [false positive](#false-positive) and discarded. + +## Model + +A machine learning model that Frigate uses to detect or classify objects. The object detection model locates [objects](#object) in each frame and returns their [labels](#label) and [bounding boxes](#bounding-box). Additional enrichment models run on tracked objects to add detail: face recognition, license plate recognition, bird classification, custom object and state classification, and the embedding models used for semantic search. [See the object detectors docs for more info](/configuration/object_detectors) ## Motion -When pixels in the current camera frame are different than previous frames. When many nearby pixels are different in the current frame they grouped together and indicated with a red motion box in the live debug view. [See the motion detection docs for more info](/configuration/motion_detection) +A change in pixels between the current camera frame and previous frames. When many nearby pixels change together, they are grouped and shown as a red motion box in the debug live view. [See the motion detection docs for more info](/configuration/motion_detection) + +## Object + +Something Frigate can detect and follow in a camera frame, identified by its [label](#label) (for example a person or a car). The object types Frigate watches for are set in the `objects` configuration. Once an object is detected and followed across frames it becomes a [tracked object](#tracked-object-event-in-previous-versions), which may also carry a [sub label](#sub-label) and [attributes](#attribute). [See the available objects docs for more info](/configuration/objects) ## Region -A portion of the camera frame that is sent to object detection, regions can be sent due to motion, active objects, or occasionally for stationary objects. These are represented by green boxes in the debug live view. +A portion of the camera frame sent to the object detection [model](#model). Regions are selected because of [motion](#motion), active objects, or occasionally to recheck stationary objects, and are shown as green boxes in the debug live view. ## Review Item -A review item is a time period where any number of events/tracked objects were active. [See the review docs for more info](/configuration/review) +A period of time during which one or more [tracked objects](#tracked-object-event-in-previous-versions) were active, grouped together for review. Each review item is categorized as either an [alert](#alert) or a [detection](#detection). [See the review docs for more info](/configuration/review) ## Snapshot Score -The score shown in a snapshot is the score of that object at that specific moment in time. +The object's score at the specific moment the snapshot was captured. + +## Sub Label + +A more specific identity assigned to a [tracked object](#tracked-object-event-in-previous-versions) in addition to its [label](#label). A `person` may get the name of a recognized face, a `car` may get the name of a known license plate, and a `bird` may get its species. An object can have only one sub label at a time. Sub labels are produced by face recognition, license plate recognition, bird classification, custom object classification configured with the `sub label` type, and semantic search triggers. ## Threshold -The threshold is the median score that an object must reach in order to be considered a true positive. +The median score an object must reach to be considered a true positive. ## Top Score -The top score for an object is the highest median score for an object. +The highest median score an object reached over its lifetime. ## Tracked Object ("event" in previous versions) -The time period starting when a tracked object entered the frame and ending when it left the frame, including any time that the object remained still. Tracked objects are saved when it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording to be saved. +An [object](#object) followed from the moment it enters the frame until it leaves, including any time it stays still. A tracked object is saved once it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording. ## Zone -Zones are areas of interest, zones can be used for notifications and for limiting the areas where Frigate will create a [review item](#review-item). [See the zone docs for more info](/configuration/zones) +A user-defined area of interest within the camera frame. Zones can be used for notifications and to limit where Frigate creates a [review item](#review-item). [See the zone docs for more info](/configuration/zones) diff --git a/docs/docs/frigate/hardware.md b/docs/docs/frigate/hardware.md index 6e98d1b7bf..1df3bb8368 100644 --- a/docs/docs/frigate/hardware.md +++ b/docs/docs/frigate/hardware.md @@ -55,7 +55,7 @@ Frigate supports multiple different detectors that work on different types of ha **Most Hardware** - [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices. - - [Supports many model architectures](../../configuration/object_detectors#configuration) + - [Supports many model architectures](../../configuration/object_detectors#configuration-hailo) - Runs best with tiny or small size models - [Google Coral EdgeTPU](#google-coral-tpu): The Google Coral EdgeTPU is available in USB and m.2 format allowing for a wide range of compatibility with devices. @@ -68,26 +68,26 @@ Frigate supports multiple different detectors that work on different types of ha **AMD** - [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection - - [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models) + - [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector) - Runs best on discrete AMD GPUs **Apple Silicon** - [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection - - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models) + - [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector) - Runs well with any size models including large - Runs via ZMQ proxy which adds some latency, only recommended for local connection **Intel** - [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection. - - [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models) + - [Supports majority of model architectures](../../configuration/object_detectors#openvino-detector) - Runs best with tiny, small, or medium models **Nvidia** - [Nvidia GPU](#nvidia-gpus): Nvidia GPUs can provide efficient object detection. - - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models) + - [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx) - Runs well with any size models including large - [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. @@ -111,14 +111,14 @@ Frigate supports multiple different detectors that work on different types of ha ### Hailo-8 -Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms—including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided. +Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided. **Default Model Configuration:** - **Hailo-8L:** Default model is **YOLOv6n**. - **Hailo-8:** Default model is **YOLOv6n**. -In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms—with dual PCIe lanes—yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup. +In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup. | Name | Hailo‑8 Inference Time | Hailo‑8L Inference Time | | ---------------- | ---------------------- | ----------------------- | diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index f69218f780..14c85ec4f3 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -78,7 +78,7 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. -The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). +The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). If raising the shm size does not help, check your [process and file limits](#process-and-file-limits) as well. The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well. @@ -86,6 +86,30 @@ The Frigate container also stores logs in shm, which can take up to **40MB**, so The shm size cannot be set per container for Home Assistant Apps. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration. +### Process and file limits + +Frigate runs many processes and opens a number of shared memory files. Installs with a large number of cameras can exceed the default limits your container runtime applies. + +Hitting the PID limit logs `RuntimeError: can't start new thread`, often followed by a "Bus error" that makes it look like an shm sizing problem. Compare the current count against the max from inside the container: + +```bash +cat /sys/fs/cgroup/pids.current +cat /sys/fs/cgroup/pids.max +``` + +If these are close, raise the limit with [`--pids-limit`](https://docs.docker.com/engine/containers/resource_constraints/) (or `service.pids_limit` in Docker Compose). + +Running out of file descriptors logs `OSError: [Errno 24] Too many open files`. Raise the limit in Docker Compose: + +```yaml +services: + frigate: + ulimits: + nofile: + soft: 65535 + hard: 65535 +``` + ## Extra Steps for Specific Hardware The following sections contain additional setup steps that are only required if you are using specific hardware. If you are not using any of these hardware types, you can skip to the [Docker](#docker) installation section. @@ -94,7 +118,7 @@ The following sections contain additional setup steps that are only required if By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options). -Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with this (affiliate link). +Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with its own power supply. Some have reported success with this (affiliate link). ### Hailo-8 @@ -484,7 +508,6 @@ Generate a Frigate Docker Compose configuration based on your hardware and requi - ```yaml diff --git a/docs/docs/frigate/network_requirements.md b/docs/docs/frigate/network_requirements.md index 2d26afbf6f..0c28f8389c 100644 --- a/docs/docs/frigate/network_requirements.md +++ b/docs/docs/frigate/network_requirements.md @@ -11,9 +11,9 @@ Frigate is designed to run locally and does not require a persistent internet co Frigate's internet usage falls into three categories: -1. **One-time model downloads** — ML models are downloaded the first time a feature is enabled, then cached locally. No internet is needed on subsequent startups. -2. **Optional cloud services** — Features like Frigate+ and Generative AI connect to external APIs only when explicitly configured. -3. **Build-time dependencies** — Components bundled into the Docker image during the build process. These require no internet at runtime. +1. **One-time model downloads**: ML models are downloaded the first time a feature is enabled, then cached locally. No internet is needed on subsequent startups. +2. **Optional cloud services**: Features like Frigate+ and Generative AI connect to external APIs only when explicitly configured. +3. **Build-time dependencies**: Components bundled into the Docker image during the build process. These require no internet at runtime. :::tip @@ -32,7 +32,7 @@ The following models are downloaded automatically the first time their associate | [License plate recognition](/configuration/license_plate_recognition) | PaddleOCR (detection, classification, recognition) + YOLOv9 plate detector | GitHub | | [Bird classification](/configuration/bird_classification) | MobileNetV2 bird model + label map | GitHub | | [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage | -| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI | +| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI | ### Hardware-Specific Detector Models @@ -91,13 +91,13 @@ See [Frigate+](/integrations/plus) for details. When a Generative AI provider is configured, Frigate sends images and prompts to the configured provider for event descriptions, chat, and camera monitoring. Available providers: -| Provider | Internet Required | -| ------------- | ---------------------------------------------------------------- | -| OpenAI | Yes — connects to OpenAI API (or custom base URL) | -| Google Gemini | Yes — connects to Google Generative AI API | -| Azure OpenAI | Yes — connects to your Azure endpoint | -| Ollama | Depends — typically local (`localhost:11434`), but can be remote | -| llama.cpp | No — runs entirely locally | +| Provider | Internet Required | +| ------------- | --------------------------------------------------------------- | +| OpenAI | Yes, connects to OpenAI API (or custom base URL) | +| Google Gemini | Yes, connects to Google Generative AI API | +| Azure OpenAI | Yes, connects to your Azure endpoint | +| Ollama | Depends: typically local (`localhost:11434`), but can be remote | +| llama.cpp | No, runs entirely locally | Disable Generative AI by removing the `genai` configuration from your cameras. See [Generative AI](/configuration/genai/genai_config) for details. @@ -126,30 +126,30 @@ When using the [DeepStack detector plugin](/configuration/object_detectors), Fri For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT traversal: -- **go2rtc** defaults to a local STUN listener (`stun:8555`) — no internet required. +- **go2rtc** defaults to a local STUN listener (`stun:8555`), no internet required. - **The web UI's WebRTC player** includes a fallback to Google's public STUN server (`stun:stun.l.google.com:19302`), which requires internet. ## Home Assistant Supervisor -When running as a Home Assistant add-on, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection. +When running as a Home Assistant App, the go2rtc startup script queries the local Supervisor API (`http://supervisor/`) to discover the host IP address and WebRTC port. This is a local network call to the Home Assistant host, not an internet connection. ## What Does NOT Require Internet -- **Object detection** — CPU, EdgeTPU, OpenVINO, and other bundled detector models are included in the Docker image. -- **Recording and playback** — All video is stored and served locally. -- **Live streaming** — Camera streams are pulled over your local network. MSE and HLS streaming work without any external connections. -- **The web interface** — Fully self-contained with no external fonts, scripts, analytics, or CDN dependencies. All translations are bundled locally. -- **Custom classification inference** — After training, custom models run entirely locally. -- **Audio detection** — The YAMNet audio classification model is bundled in the Docker image. +- **Object detection**: CPU, EdgeTPU, OpenVINO, and other bundled detector models are included in the Docker image. +- **Recording and playback**: All video is stored and served locally. +- **Live streaming**: Camera streams are pulled over your local network. MSE and HLS streaming work without any external connections. +- **The web interface**: Fully self-contained with no external fonts, scripts, analytics, or CDN dependencies. All translations are bundled locally. +- **Custom classification inference**: After training, custom models run entirely locally. +- **Audio detection**: The YAMNet audio classification model is bundled in the Docker image. ## Running Frigate Offline To run Frigate in an air-gapped or offline environment: -1. **Pre-download models** — Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`. -2. **Disable version check** — Set `telemetry.version_check: false` in your configuration. -3. **Block outbound model requests** — Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests. -4. **Avoid cloud features** — Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers. -5. **Use local model mirrors** — If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors. +1. **Pre-download models**: Start Frigate with internet access once with all desired features enabled. Models will be cached in `/config/model_cache/`. +2. **Disable version check**: Set `telemetry.version_check: false` in your configuration. +3. **Block outbound model requests**: Set the `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` environment variables to prevent HuggingFace and Transformers from attempting any network requests. +4. **Avoid cloud features**: Do not configure Frigate+, Generative AI providers that require internet, or cloud MQTT brokers. +5. **Use local model mirrors**: If limited internet is available, set the `HF_ENDPOINT`, `GITHUB_ENDPOINT`, and `GITHUB_RAW_ENDPOINT` environment variables to point to local mirrors. After these steps, Frigate will operate with no outbound internet connections. diff --git a/docs/docs/frigate/planning_setup.md b/docs/docs/frigate/planning_setup.md index 85c6eb6482..4e0d82ffb7 100644 --- a/docs/docs/frigate/planning_setup.md +++ b/docs/docs/frigate/planning_setup.md @@ -42,6 +42,8 @@ Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011 Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings. +Once running, see [Understanding storage usage](/configuration/record#understanding-storage-usage) for how Frigate measures and reports disk usage, and why its numbers won't exactly match `df` or `du`. + #### SSDs (Solid State Drives) SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs. diff --git a/docs/docs/guides/getting_started.md b/docs/docs/guides/getting_started.md index aa52a3e3e5..fe355abe52 100644 --- a/docs/docs/guides/getting_started.md +++ b/docs/docs/guides/getting_started.md @@ -305,7 +305,7 @@ Restart Frigate and you should start seeing detections for `person`. If you want ### Step 5: Setup motion masks -Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. Click on the camera from the main dashboard, then select the gear icon in the top right, enable Debug View, and finally enable the switch for Motion Boxes. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas. +Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. Click on the camera from the main dashboard, then select the gear icon in the top right, enable the [Debug view](/usage/live#the-single-camera-view), and finally enable the switch for Motion Boxes. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas. Use the mask editor to draw polygon masks directly on the camera feed. Navigate to and set up a motion mask over the area. More information about masks can be found [here](../configuration/masks.md). @@ -348,7 +348,7 @@ In order to review activity in the Frigate UI, recordings need to be enabled. -1. If you have separate streams for detect and record, navigate to , select your camera, and add a second input with the `record` role pointing to your high-resolution stream +1. If you have separate streams for detect and record, navigate to , select your camera, and add a second input with the `record` role pointing to your high-resolution stream 2. Navigate to (or for a specific camera) and set **Enable recording** to on diff --git a/docs/docs/guides/reverse_proxy.md b/docs/docs/guides/reverse_proxy.md index e40ce45196..bf33bcc278 100644 --- a/docs/docs/guides/reverse_proxy.md +++ b/docs/docs/guides/reverse_proxy.md @@ -10,13 +10,14 @@ A reverse proxy is typically needed if you want to set up Frigate on a custom UR Before setting up a reverse proxy, check if any of the built-in functionality in Frigate suits your needs: |Topic|Docs| |-|-| -|TLS|Please see the `tls` [configuration option](../configuration/tls.md)| +|TLS|Please see the `tls` [configuration option](../configuration/tls.md)| |Authentication|Please see the [authentication](../configuration/authentication.md) documentation| |IPv6|[Enabling IPv6](../configuration/advanced/system.md#enabling-ipv6) -**Note about TLS** -When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port." +**Note about TLS** +When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port." To disable TLS, set the following in your Frigate configuration: + ```yml tls: enabled: false @@ -24,18 +25,26 @@ tls: :::warning A reverse proxy can be used to secure access to an internal web server, but the user will be entirely reliant on the steps they have taken. You must ensure you are following security best practices. -This page does not attempt to outline the specific steps needed to secure your internal website. +This page does not attempt to outline the specific steps needed to secure your internal website. Please use your own knowledge to assess and vet the reverse proxy software before you install anything on your system. ::: +## WebSocket support + +Frigate relies on WebSockets for real-time communication between the browser and the backend. Features such as camera controls (enabling/disabling a camera, audio, detect, recordings, and other toggles), live stream playback, and other live-updating parts of the UI will not function correctly if WebSocket connections are not proxied. + +Your reverse proxy must be configured to forward the `Upgrade` and `Connection` headers so that WebSocket connections can be established. Each proxy example below already includes the directives needed to do this, but if you are adapting your own configuration, ensure these headers are passed through. + +Note that some proxies disable WebSocket support by default. For example, Nginx Proxy Manager has a "Websockets Support" toggle that must be enabled. + ## Proxies There are many solutions available to implement reverse proxies and the community is invited to help out documenting others through a contribution to this page. -* [Apache2](#apache2-reverse-proxy) -* [Nginx](#nginx-reverse-proxy) -* [Traefik](#traefik-reverse-proxy) -* [Caddy](#caddy-reverse-proxy) +- [Apache2](#apache2-reverse-proxy) +- [Nginx](#nginx-reverse-proxy) +- [Traefik](#traefik-reverse-proxy) +- [Caddy](#caddy-reverse-proxy) ## Apache2 Reverse Proxy @@ -159,7 +168,7 @@ The settings below enabled connection upgrade, sets up logging (optional) and pr ## Traefik Reverse Proxy -This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance. +This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance. Before using the example below, you must first set up Traefik with the [Docker provider](https://doc.traefik.io/traefik/providers/docker/) ```yml @@ -203,7 +212,7 @@ This example shows Frigate running under a subdomain with logging and a tls cert } frigate.YOUR_DOMAIN.TLD { - reverse_proxy http://localhost:8971 + reverse_proxy http://localhost:8971 import tls import logging frigate.YOUR_DOMAIN.TLD } diff --git a/docs/docs/integrations/home-assistant.md b/docs/docs/integrations/home-assistant.md index 14ee795289..dd7d548047 100644 --- a/docs/docs/integrations/home-assistant.md +++ b/docs/docs/integrations/home-assistant.md @@ -9,6 +9,8 @@ The best way to integrate with Home Assistant is to use the [official integratio ### Preparation +Frigate itself must be installed and running before setting up the integration. See the [installation documentation](../frigate/installation.md) for details. + The Frigate integration requires the `mqtt` integration to be installed and manually configured first. @@ -122,7 +124,7 @@ Use `http://:8971` as the URL for the integration so that aut The above URL assumes you have [disabled TLS](../configuration/tls). By default, TLS is enabled and Frigate will be using a self-signed certificate. HomeAssistant will fail to connect HTTPS to port 8971 since it fails to verify the self-signed certificate. -Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be acessible with a valid certificate. +Either disable TLS and use HTTP from HomeAssistant, or configure Frigate to be accessible with a valid certificate. ::: diff --git a/docs/docs/integrations/mqtt.md b/docs/docs/integrations/mqtt.md index 44d6ecee0b..e929ade300 100644 --- a/docs/docs/integrations/mqtt.md +++ b/docs/docs/integrations/mqtt.md @@ -280,7 +280,7 @@ Same data available at `/api/stats` published at a configurable interval. ### `frigate/camera_activity` -Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publising to `frigate/onConnect` +Returns data about each camera, its current features, and if it is detecting motion, objects, etc. Can be triggered by publishing to `frigate/onConnect` ### `frigate/profile/set` diff --git a/docs/docs/integrations/third_party_extensions.md b/docs/docs/integrations/third_party_extensions.md index a7d5fc9e61..64b174a042 100644 --- a/docs/docs/integrations/third_party_extensions.md +++ b/docs/docs/integrations/third_party_extensions.md @@ -23,7 +23,7 @@ The [Advanced Camera Card](https://card.camera/#/README) is a Home Assistant das ## [Double Take](https://github.com/skrashevich/double-take) -[Double Take](https://github.com/skrashevich/double-take) provides an unified UI and API for processing and training images for facial recognition. +[Double Take](https://github.com/skrashevich/double-take) provides a unified UI and API for processing and training images for facial recognition. It supports automatically setting the sub labels in Frigate for person objects that are detected and recognized. This is a fork (with fixed errors and new features) of [original Double Take](https://github.com/jakowenko/double-take) project which, unfortunately, isn't being maintained by author. @@ -31,6 +31,10 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht [Frigate Notify](https://github.com/0x2142/frigate-notify) is a simple app designed to send notifications from Frigate to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended. +## [Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert) + +[Frigate Notify Alert](https://github.com/Sysoev86/frigate-notify-alert) sends Frigate events to Telegram as a photo + video media group. It supports multiple camera groups (each notifying its own chat), optional zone filtering (notify only when an object enters a chosen zone), and in-chat buttons to pause notifications for a set time. Works with standalone Frigate over MQTT; Home Assistant not required. + ## [Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) [Frigate Snap-Sync](https://github.com/thequantumphysicist/frigate-snap-sync/) is a program that works in tandem with Frigate. It responds to Frigate when a snapshot or a review is made (and more can be added), and uploads them to one or more remote server(s) of your choice. @@ -49,7 +53,7 @@ This is a fork (with fixed errors and new features) of [original Double Take](ht ## [Scrypted - Frigate bridge plugin](https://github.com/apocaliss92/scrypted-frigate-bridge) -[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is an plugin that allows to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate. +[Scrypted - Frigate bridge](https://github.com/apocaliss92/scrypted-frigate-bridge) is a plugin that allows you to ingest Frigate detections, motion, videoclips on Scrypted as well as provide templates to export rebroadcast configurations on Frigate. ## [Strix](https://github.com/eduard256/Strix) diff --git a/docs/docs/plus/annotating.md b/docs/docs/plus/annotating.md index dc8e571be4..3725ab5c2c 100644 --- a/docs/docs/plus/annotating.md +++ b/docs/docs/plus/annotating.md @@ -19,7 +19,7 @@ For the best results, follow these guidelines. You may also want to review the d ## AI suggested labels -If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for you camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer. +If you have an active Frigate+ subscription, new uploads will be scanned for the objects configured for your camera and you will see suggested labels as light blue boxes when annotating in Frigate+. These suggestions are processed via a queue and typically complete within a minute after uploading, but processing times can be longer. ![Suggestions](/img/plus/suggestions.webp) diff --git a/docs/docs/plus/first_model.md b/docs/docs/plus/first_model.md index e9523f6b98..98095554b5 100644 --- a/docs/docs/plus/first_model.md +++ b/docs/docs/plus/first_model.md @@ -3,6 +3,10 @@ id: first_model title: Requesting your first model --- +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import NavPath from "@site/src/components/NavPath"; + ## Step 1: Upload and annotate your images Before requesting your first model, you will need to upload and verify at least 10 images to Frigate+. The more images you upload, annotate, and verify the better your results will be. Most users start to see very good results once they have at least 100 verified images per camera. Keep in mind that varying conditions should be included. You will want images from cloudy days, sunny days, dawn, dusk, and night. Refer to the [integration docs](../integrations/plus.md#generate-an-api-key) for instructions on how to easily submit images to Frigate+ directly from Frigate. @@ -16,13 +20,21 @@ For more detailed recommendations, you can refer to the docs on [annotating](./a Once you have an initial set of verified images, you can request a model on the Models page. For guidance on choosing a model type, refer to [this part of the documentation](./index.md#available-model-types). If you are unsure which type to request, you can test the base model for each version from the "Base Models" tab. Each model request requires 1 of the 12 trainings that you receive with your annual subscription. This model will support all [label types available](./index.md#available-label-types) even if you do not submit any examples for those labels. Model creation can take up to 36 hours. ![Plus Models Page](/img/plus/plus-models.jpg) -## Step 3: Set your model id in the config +## Step 3: Set your model You will receive an email notification when your Frigate+ model is ready. ![Model Ready Email](/img/plus/model-ready-email.jpg) Models available in Frigate+ can be used with a special model path. No other information needs to be configured because it fetches the remaining config from Frigate+ automatically. + + + +Navigate to . In the **Detection Model** section, choose the **Frigate+** tab. Select your new Frigate+ model from the **Available Frigate+ models** dropdown, then click **Save**. Restart Frigate to apply the change. + + + + ```yaml detectors: ... @@ -30,22 +42,46 @@ model: path: plus:// ``` -:::note - -Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. - -::: - :::tip When setting the plus model id, all other fields should be removed as these are configured automatically with the Frigate+ model config ::: + + + +:::note + +Model IDs are not secret values and can be shared freely. Access to your model is protected by your API key. + +::: + ## Step 4: Adjust your object filters for higher scores Frigate+ models generally have much higher scores than the default model provided in Frigate. You will likely need to increase your `threshold` and `min_score` values. Here is an example of how these values can be refined, but you should expect these to evolve as your model improves. For more information about how `threshold` and `min_score` are related, see the docs on [object filters](../configuration/object_filters.md#object-scores). + + + +Navigate to . Under **Object filters**, set **Min Score** and **Threshold** for each object type, then click **Save**. + +| Object | Min Score | Threshold | +| ----------------- | --------- | --------- | +| **dog** | .7 | .9 | +| **cat** | .65 | .8 | +| **face** | .7 | | +| **package** | .65 | .9 | +| **license_plate** | .6 | | +| **amazon** | .75 | | +| **ups** | .75 | | +| **fedex** | .75 | | +| **person** | .65 | .85 | +| **car** | .65 | .85 | + + + + ```yaml objects: filters: @@ -75,3 +111,6 @@ objects: min_score: .65 threshold: .85 ``` + + + diff --git a/docs/docs/troubleshooting/cpu.md b/docs/docs/troubleshooting/cpu.md index 18ae8b7013..a6c2444a5b 100644 --- a/docs/docs/troubleshooting/cpu.md +++ b/docs/docs/troubleshooting/cpu.md @@ -44,7 +44,7 @@ Choosing the right detector for your hardware is the single most important facto ### Understanding Detector Performance -Frigate uses motion detection as a first-line check before running expensive object detection, as explained in the [motion detection documentation](../configuration/motion_detection). When motion is detected, Frigate creates a "region" (the green boxes in the debug viewer) and sends it to the detector. The detector's inference speed determines how many detections per second your system can handle. +Frigate uses motion detection as a first-line check before running expensive object detection, as explained in the [motion detection documentation](../configuration/motion_detection). When motion is detected, Frigate creates a "region" (the green boxes in the [debug viewer](/usage/live#the-single-camera-view)) and sends it to the detector. The detector's inference speed determines how many detections per second your system can handle. **Calculating Detector Capacity:** Your detector has a finite capacity measured in detections per second. With an inference speed of 10ms, your detector can handle approximately 100 detections per second (1000ms / 10ms = 100).If your cameras collectively require more than this capacity, you'll experience delays, missed detections, or the system will fall behind. @@ -58,7 +58,6 @@ When a single detector cannot keep up with your camera count, some detector type For detailed instructions on configuring multiple detectors, see the [Object Detectors documentation](../configuration/object_detectors). - **When to add a second detector:** - Skipped FPS is consistently > 0 even during normal activity @@ -70,4 +69,6 @@ The model you use significantly impacts detector performance. Frigate provides d **Model Size Trade-offs:** - Smaller models (320x320): Faster inference, Frigate is specifically optimized for a 320x320 size model. -- Larger models (640x640): Slower inference, can sometimes have higher accuracy on very large objects that take up a majority of the frame. \ No newline at end of file +- Larger models (640x640): Slower inference, can sometimes have higher accuracy on very large objects that take up a majority of the frame. + +For more detail on picking the right size, see [Choosing a model size](../configuration/object_detectors.md#choosing-a-model-size). diff --git a/docs/docs/troubleshooting/dummy-camera.md b/docs/docs/troubleshooting/dummy-camera.md index 4443d11e81..630a7921d5 100644 --- a/docs/docs/troubleshooting/dummy-camera.md +++ b/docs/docs/troubleshooting/dummy-camera.md @@ -39,7 +39,9 @@ The per-clip variation is typically quite low and is mostly an artifact of keyfr Debug Replay lets you re-run Frigate's detection pipeline against a section of recorded video without manually configuring a dummy camera. It automatically extracts the recording, creates a temporary camera with the same detection settings as the original, and loops the clip through the pipeline so you can observe detections in real time. -Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real-time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature. +The replay camera behaves like a live camera feed rather than History's video player: it loops the clip continuously as Frigate analyzes it and has no playback controls, so you cannot pause, scrub, or step through it frame by frame. + +Debug Replay isn't intended to be a one-stop pane for all Frigate diagnostics or a comprehensive debugging environment for every Frigate feature. It merely makes it easier to spin up a "dummy camera" and perform some common adjustments in real time. You'll still need to use the normal tools (logs, an MQTT client, etc) to debug your feature. ### When to use @@ -57,11 +59,11 @@ Only one replay session can be active at a time. If a session is already running Debug Replay can be started from several places in the UI. The starting point determines the time range that gets replayed. -- **History — Actions menu.** Navigate to , open the **Actions** menu in the toolbar, and choose **Debug Replay**. From here you can pick a preset (**Last 1 Minute**, **Last 5 Minutes**), select a range directly on the timeline with **From Timeline**, or enter exact start and end times with **Custom**. This is the most flexible option and the best choice when you want to add padding around a detection. On mobile, the same options appear in the Actions drawer. -- **History — Detail Stream event menu.** While viewing a review item in the Detail Stream, open the menu on a tracked object's event card and choose **Debug Replay**. The replay range is set automatically to that object's start and end times. -- **Explore — search result menu.** From an Explore card, open the kebab menu and choose **Debug Replay**. The range is taken from the tracked object's lifecycle. -- **Explore — Tracking Details Actions menu.** Open a tracked object's **Tracking Details** dialog, then choose **Debug Replay** from the Actions menu. Same automatic range as the search result menu. -- **Exports — export card menu.** From , open the menu on an export and choose **Debug Replay** to loop the exported clip through the detection pipeline for the camera it was exported from. +- **History: Actions menu.** Navigate to , open the **Actions** menu in the toolbar, and choose **Debug Replay**. From here you can pick a preset (**Last 1 Minute**, **Last 5 Minutes**), select a range directly on the timeline with **From Timeline**, or enter exact start and end times with **Custom**. This is the most flexible option and the best choice when you want to add padding around a detection. On mobile, the same options appear in the Actions drawer. +- **History: Detail Stream event menu.** While viewing a review item in the Detail Stream, open the menu on a tracked object's event card and choose **Debug Replay**. The replay range is set automatically to that object's start and end times. +- **Explore: search result menu.** From an Explore card, open the kebab menu and choose **Debug Replay**. The range is taken from the tracked object's lifecycle. +- **Explore: Tracking Details Actions menu.** Open a tracked object's **Tracking Details** dialog, then choose **Debug Replay** from the Actions menu. Same automatic range as the search result menu. +- **Exports: export card menu.** From , open the menu on an export and choose **Debug Replay** to loop the exported clip through the detection pipeline for the camera it was exported from. The Detail Stream, Explore, and Exports entry points use the underlying recording or export's bounds with a small amount of padding. This can be convenient for quick checks, but if a detection is short or you want extra "settle" time for motion and the detector, start the replay from the History Actions menu instead and widen the range manually. @@ -77,7 +79,7 @@ Treat the replay as a close approximation rather than an exact reproduction. Run ## Manual Dummy Camera -For advanced scenarios — such as testing with a clip from a different source, debugging ffmpeg behavior, or running a clip through a completely custom configuration — you can set up a dummy camera manually. +For advanced scenarios (such as testing with a clip from a different source, debugging ffmpeg behavior, or running a clip through a completely custom configuration), you can set up a dummy camera manually. ### Example config @@ -109,7 +111,7 @@ cameras: 2. Add the temporary camera to `config/config.yml` (example above). Use a unique name such as `test` or `replay_camera` so it's easy to remove later. - If you're debugging a specific camera, copy the settings from that camera (frame rate, model/enrichment settings, zones, etc.) into the temporary camera so the replay closely matches the original environment. Leave `record` and `snapshots` disabled unless you are specifically debugging recording or snapshot behavior. 3. Restart Frigate. -4. Observe the Debug view in the UI and logs as the clip is replayed. Watch detections, zones, or any feature you're looking to debug, and note any errors in the logs to reproduce the issue. +4. Observe the [Debug view](/usage/live#the-single-camera-view) in the UI and logs as the clip is replayed. Watch detections, zones, or any feature you're looking to debug, and note any errors in the logs to reproduce the issue. 5. Iterate on camera or enrichment settings (model, fps, zones, filters) and re-check the replay until the behavior is resolved. 6. Remove the temporary camera from your config after debugging to avoid spurious telemetry or recordings. diff --git a/docs/docs/troubleshooting/edgetpu.md b/docs/docs/troubleshooting/edgetpu.md index 4ee25afd0f..bc50ae5040 100644 --- a/docs/docs/troubleshooting/edgetpu.md +++ b/docs/docs/troubleshooting/edgetpu.md @@ -12,7 +12,7 @@ There are many possible causes for a USB coral not being detected and some are O :::tip -Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inferance using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral. +Using `lsusb` or checking the hardware page in HA OS will show as `1a6e:089a Global Unichip Corp.` until Frigate runs an inference using the coral. So don't worry about the identification until after Frigate has attempted to detect the coral. ::: @@ -43,13 +43,13 @@ Some users have reported that this older device runs an older kernel causing iss 3. Start the docker container with Coral TPU enabled in the config 4. The TPU would be detected but a few moments later it would disconnect. 5. While leaving the TPU device plugged in, restart the NAS using the reboot command in the UI. Do NOT unplug the NAS/power it off etc. -6. Open the control panel - info scree. The coral TPU will now be recognised as a USB Device - google inc +6. Open the control panel - info screen. The coral TPU will now be recognized as a USB Device - google inc 7. Start the frigate container. Everything should work now! ### QNAP NAS -QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features—`facial recognition`, `object recognition`, or `similar photo recognition`—are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use. -To allow the Coral TPU device to be discovered, the you must either: +QNAP NAS devices, such as the TS-253A, may use connected Coral TPU devices if [QuMagie](https://www.qnap.com/en/software/qumagie) is installed along with its QNAP AI Core extension. If any of the features (`facial recognition`, `object recognition`, or `similar photo recognition`) are enabled, Container Station applications such as `Frigate` or `CodeProject.AI Server` will be unable to initialize the TPU device in use. +To allow the Coral TPU device to be discovered, you must either: 1. [Disable the AI recognition features in QuMagie](https://docs.qnap.com/application/qumagie/2.x/en-us/configuring-qnap-ai-core-settings-FB13CE03.html), 2. Remove the QNAP AI Core extension or @@ -76,7 +76,7 @@ This is an issue due to outdated gasket driver when being used with new linux ke ### Not detected on Raspberry Pi5 -A kernel update to the RPi5 means an upate to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25) +A kernel update to the RPi5 means an update to config.txt is required, see [the raspberry pi forum for more info](https://forums.raspberrypi.com/viewtopic.php?t=363682&sid=cb59b026a412f0dc041595951273a9ca&start=25) Specifically, add the following to config.txt @@ -87,7 +87,7 @@ dtoverlay=pcie-32bit-dma-pi5 ## Only One PCIe Coral Is Detected With Coral Dual EdgeTPU -Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has it's own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work. +Coral Dual EdgeTPU is one card with two identical TPU cores. Each core has its own PCIe interface and motherboard needs to have two PCIe busses on the m.2 slot to make them both work. E-key slot implemented to full m.2 electromechanical specification has two PCIe busses. Most motherboard manufacturers implement only one PCIe bus in m.2 E-key connector (this is why only one TPU is working). Some SBCs can have only USB bus on m.2 connector, ie none of TPUs will work. diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md index 8fee36b83b..1a51cb64e2 100644 --- a/docs/docs/troubleshooting/faqs.md +++ b/docs/docs/troubleshooting/faqs.md @@ -29,7 +29,7 @@ You can open `chrome://media-internals/` in another tab and then try to playback ### What do I do if my cameras sub stream is not good enough? -Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that a suitable resolution, the main stream can be resized. +Frigate generally [recommends cameras with configurable sub streams](/frigate/hardware.md). However, if your camera does not have a sub stream that is a suitable resolution, the main stream can be resized. To do this efficiently the following setup is required: @@ -49,9 +49,9 @@ This almost always means that the width/height defined for your camera are not c These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically. -### "On connect called" +### "MQTT connected" repeats in the logs -If you see repeated "On connect called" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`. +If you see repeated "MQTT connected" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`. ### Error: Database Is Locked @@ -124,3 +124,32 @@ cameras: width: 1280 height: 720 ``` + +### Why does Frigate keep creating new tracked objects for my parked car? + +Stationary tracking is designed to _prevent_ this: a parked car should remain a single tracked object rather than generating new ones. If you're repeatedly getting new tracked objects for the same car, it's likely that Frigate is losing the object and re-detecting it as a new one. + +Open one of the tracked objects 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. + +### How do I correct Frigate when it detects something as the wrong object? + +Frigate's object detection relies on a machine learning [model](../frigate/glossary.md#model), and the free [COCO-trained](https://cocodataset.org/#explore) models that ship with Frigate can misidentify objects in scenes they weren't trained on. There are two ways to handle this, depending on whether you want to _teach_ the model or just _suppress_ the bad result. + +**Train or fine-tune a model with your own images.** The most durable fix is to improve the model itself. The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. Some tools are freely available, and [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. When Frigate mislabels something, open the tracked object in Explore, select the **Snapshot** tab, and use **Submit to Frigate+** to send the example with the correct label (or mark it as a [false positive](../frigate/glossary.md#false-positive)). Once you've submitted examples and [requested a model](../plus/first_model.md), the retrained model will be more accurate for your cameras. See [Submitting examples to Frigate+](../integrations/plus.md#submit-examples) for the full workflow. + +**Suppress the misidentification with filters.** You can use filters to stop a specific false positive from being tracked: + +- Tune `min_score` / `threshold`, or add `min_area` / `max_area` / `min_ratio` / `max_ratio` filters. See [Object Filters](../configuration/object_filters.md). +- If the false positive is always in the same fixed spot (like a statue or mailbox that reads as a person), add an [object filter mask](../configuration/masks.md#object-filter-masks) over that location. + +Filters and masks only hide the incorrect result - they don't teach Frigate what the object actually is. For that, fine-tune your own model or use Frigate+. diff --git a/docs/docs/troubleshooting/go2rtc.md b/docs/docs/troubleshooting/go2rtc.md index 4a9a151aad..b8f2102406 100644 --- a/docs/docs/troubleshooting/go2rtc.md +++ b/docs/docs/troubleshooting/go2rtc.md @@ -9,29 +9,29 @@ import NavPath from "@site/src/components/NavPath"; This page covers common problems with the bundled [go2rtc](/configuration/go2rtc) and how to resolve them, whether your cameras were added with the setup wizard or configured by hand. -When a stream won't play or behaves oddly, the most important first step is to figure out **where** in the pipeline it breaks. Frigate's live view is a chain — _camera → go2rtc → your browser_ — and each stage fails for different reasons. Work through the checks below in order, then jump to the matching problem category. +When a stream won't play or behaves oddly, the most important first step is to figure out **where** in the pipeline it breaks. Frigate's live view is a chain (_camera → go2rtc → your browser_), and each stage fails for different reasons. Work through the checks below in order, then jump to the matching problem category. ## Start by isolating the problem ### 1. Read the go2rtc logs -Access the go2rtc logs in the Frigate UI under in the sidebar (select the **go2rtc** tab). If go2rtc cannot connect to your camera you will usually see a clear error here — `401 Unauthorized` (bad or incorrectly encoded credentials), `Connection refused` / `timeout` (wrong IP, port, or the camera is at its connection limit), or `404 Not Found` (wrong RTSP path, or the referenced stream name does not exist). +Access the go2rtc logs in the Frigate UI under in the sidebar (select the **go2rtc** tab). If go2rtc cannot connect to your camera you will usually see a clear error here: `401 Unauthorized` (bad or incorrectly encoded credentials), `Connection refused` / `timeout` (wrong IP, port, or the camera is at its connection limit), or `404 Not Found` (wrong RTSP path, or the referenced stream name does not exist). ### 2. Test the stream in the go2rtc web interface If the logs look clean, open go2rtc's own web interface on port `1984`. This is the single most useful diagnostic, because it takes Frigate's UI out of the equation entirely. -- If using Frigate through Home Assistant, enable the web interface at port `1984` (it is disabled by default — see [Home Assistant ports](#home-assistant-and-port-access)). +- If using Frigate through Home Assistant, enable the web interface at port `1984` (it is disabled by default, see [Home Assistant ports](#home-assistant-and-port-access)). - If using Docker, forward port `1984` before accessing the web interface. Open the stream page for your camera (`http://:1984/stream.html?src=back`) and try each player link: - **If nothing plays here**, the problem is between the camera and go2rtc (codec, credentials, or transport), _not_ your browser. Fix it at the source before touching anything in Frigate. -- **If a player works here but Frigate's live view does not**, the problem is browser/codec related — compare the **MSE** and **WebRTC** links. Frigate prefers MSE and only attempts WebRTC when MSE fails (or for two-way talk). If `mode=mse` plays but `mode=webrtc` does not, you have a [WebRTC codec problem](#webrtc-and-two-way-talk); if neither plays, your browser cannot decode the codec (commonly H.265 — see [H.265 / HEVC cameras](#h265--hevc-cameras)). +- **If a player works here but Frigate's live view does not**, the problem is browser/codec related. Compare the **MSE** and **WebRTC** links. Frigate prefers MSE and only attempts WebRTC when MSE fails (or for two-way talk). If `mode=mse` plays but `mode=webrtc` does not, you have a [WebRTC codec problem](#webrtc-and-two-way-talk); if neither plays, your browser cannot decode the codec (commonly H.265, see [H.265 / HEVC cameras](#h265--hevc-cameras)). ### 3. Inspect the negotiated codecs -You can view detailed stream info — including the exact video and audio codecs go2rtc negotiated with the camera — at `http://frigate_ip:5000/api/go2rtc/streams` (or `http://frigate_ip:5000/api/go2rtc/streams/back` for a single camera). This is the authoritative answer to "what is my camera actually sending?" and is far more reliable than guessing from the camera's web UI. It also shows whether the audio track is `sendonly`/`recvonly`, which matters for [two-way talk](#webrtc-and-two-way-talk). +You can view detailed stream info, including the exact video and audio codecs go2rtc negotiated with the camera, at `http://frigate_ip:5000/api/go2rtc/streams` (or `http://frigate_ip:5000/api/go2rtc/streams/back` for a single camera). This is the authoritative answer to "what is my camera actually sending?" and is far more reliable than guessing from the camera's web UI. It also shows whether the audio track is `sendonly`/`recvonly`, which matters for [two-way talk](#webrtc-and-two-way-talk). ### 4. Fix the codec with the FFmpeg module @@ -44,7 +44,7 @@ In the Frigate UI this is the **Use compatibility mode (ffmpeg)** toggle on a st 1. Navigate to and expand your camera's stream. 2. On the source you want to convert, click the **Use compatibility mode (ffmpeg)** button (the sliders icon next to the URL). This routes the source through go2rtc's FFmpeg module and reveals the transcoding options. -3. Set **Video** to **Transcode to H.264** if your browser can't play the camera's video codec (e.g. H.265). Leave it on **Copy** to pass the video through untouched — this is much cheaper and should be your default whenever only the audio needs converting. +3. Set **Video** to **Transcode to H.264** if your browser can't play the camera's video codec (e.g. H.265). Leave it on **Copy** to pass the video through untouched. This is much cheaper and should be your default whenever only the audio needs converting. 4. Set **Audio** to **Transcode to AAC** (for MSE) or **Transcode to Opus** (for WebRTC) if the camera's audio codec is unsupported. Leave it on **Copy** to keep the original, or **Exclude** to drop audio entirely. 5. When transcoding **video**, set **Hardware acceleration** to **Automatic (recommended)** so the encode runs on your GPU instead of the CPU. See [hardware-accelerated transcoding](#hardware-accelerated-transcoding-with-ffmpeg-8) for an important FFmpeg 8 caveat. 6. **Save** the section, then reload the live view. @@ -68,7 +68,7 @@ go2rtc: streams: back: - rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 - - "ffmpeg:back#audio=aac" # audio only — preferred when the video already plays + - "ffmpeg:back#audio=aac" # audio only, preferred when the video already plays # or, to convert both video and audio: # - "ffmpeg:back#video=h264#audio=aac#hardware" ``` @@ -78,7 +78,7 @@ go2rtc: :::warning -The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing — go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you. +The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing: go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you. ::: @@ -86,13 +86,13 @@ Transcoding video is resource intensive. Always prefer `#video=copy` (the **Copy ## Live view is black, buffering, or stuck in "low-bandwidth mode" -When the live view shows a black screen, spins forever, or repeatedly drops to the lower-quality jsmpeg player ("low-bandwidth mode"), the stream almost always contains something the browser cannot decode over MSE — usually H.265 video or a non-AAC audio track. Confirm this in the go2rtc web UI (port `1984`): if MSE won't play there, Frigate can't play it either, since it uses the same pipeline. +When the live view shows a black screen, spins forever, or repeatedly drops to the lower-quality jsmpeg player ("low-bandwidth mode"), the stream almost always contains something the browser cannot decode over MSE, usually H.265 video or a non-AAC audio track. Confirm this in the go2rtc web UI (port `1984`): if MSE won't play there, Frigate can't play it either, since it uses the same pipeline. The fix is to produce an **H.264 + AAC** stream, either by changing your camera's firmware codecs or by transcoding in go2rtc (see [Fix the codec with the FFmpeg module](#4-fix-the-codec-with-the-ffmpeg-module)). A few other things worth checking: - **Set the camera's I-frame (keyframe) interval to match its frame rate** (or "1x" on Reolink), and avoid "smart"/"+" codecs like _H.264+_ or _H.265+_. A long keyframe interval delays the first decodable frame past Frigate's startup timeout, which forces the fallback to jsmpeg. See [camera settings recommendations](/configuration/live#camera-settings-recommendations). - **A spinner that never clears, even though video plays in VLC**, is often an unplayable _audio_ track stalling playback. Drop or transcode the audio (see below). -- **Remote/VPN viewing that buffers** while the LAN is fine is usually latency/jitter exceeding MSE's startup buffer — set up [WebRTC](/configuration/live#webrtc-extra-configuration), which drops late frames instead of buffering. +- **Remote/VPN viewing that buffers** while the LAN is fine is usually latency/jitter exceeding MSE's startup buffer. Set up [WebRTC](/configuration/live#webrtc-extra-configuration), which drops late frames instead of buffering. The general live-view behavior (smart streaming, the MSE → WebRTC → jsmpeg fallback chain, and how to read browser console errors) is documented in detail in the [Live view FAQ](/configuration/live#live-view-faq). @@ -151,25 +151,25 @@ Setting the camera firmware to AAC (and H.264) avoids transcoding entirely and i WebRTC is only attempted when MSE fails or when using a camera's two-way talk feature; the "All Cameras" dashboard never uses it. When it doesn't work, the cause is almost always one of: -- **Codec mismatch** — WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above. -- **Port `8555` not reachable, or no candidates set** — WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration). -- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly — go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream). +- **Codec mismatch**: WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above. +- **Port `8555` not reachable, or no candidates set**: WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration). +- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly: go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream). ## High CPU usage -If go2rtc is using a lot of CPU, it is almost always transcoding in software. An FFmpeg source with a codec modifier like `#video=h264` or `#audio=aac` but **no** `#hardware` re-encodes on the CPU. (Frigate's `ffmpeg.hwaccel_args` only applies to Frigate's own detect/record processes — it does _not_ accelerate go2rtc's transcodes.) +If go2rtc is using a lot of CPU, it is almost always transcoding in software. An FFmpeg source with a codec modifier like `#video=h264` or `#audio=aac` but **no** `#hardware` re-encodes on the CPU. (Frigate's `ffmpeg.hwaccel_args` only applies to Frigate's own detect/record processes. It does _not_ accelerate go2rtc's transcodes.) To keep CPU usage down: - Only transcode the track that is genuinely unsupported, and use `#video=copy` to pass video through untouched whenever possible. - When you must transcode video, always add `#hardware` (the **Automatic** hardware option in the UI) so the encode runs on the GPU. Note the [FFmpeg 8 device requirement](#hardware-accelerated-transcoding-with-ffmpeg-8) below. -- Don't restream a high-resolution main stream just to feed the live view — even with `#video=copy`, muxing a 4K/8MP+ stream is inherently expensive. Use the camera's lower-resolution substream for live and detect, and let Frigate pull the main stream directly for recording. +- Don't restream a high-resolution main stream just to feed the live view: even with `#video=copy`, muxing a 4K/8MP+ stream is inherently expensive. Use the camera's lower-resolution substream for live and detect, and let Frigate pull the main stream directly for recording. ## Connection, authentication, and complex passwords -If go2rtc logs `401 Unauthorized` for a URL that works in VLC, the password almost certainly contains reserved URL characters. **Frigate URL-encodes passwords for its own `cameras.ffmpeg.inputs`, but it does not touch what you write under `go2rtc.streams`** — go2rtc parses that URL itself. You must URL-encode special characters yourself in the `go2rtc.streams` section (`@` → `%40`, `#` → `%23`, `?` → `%3F`, `%` → `%25`, etc.). +If go2rtc logs `401 Unauthorized` for a URL that works in VLC, the password almost certainly contains reserved URL characters. **Frigate URL-encodes passwords for its own `cameras.ffmpeg.inputs`, but it does not touch what you write under `go2rtc.streams`**: go2rtc parses that URL itself. You must URL-encode special characters yourself in the `go2rtc.streams` section (`@` → `%40`, `#` → `%23`, `?` → `%3F`, `%` → `%25`, etc.). -Note the asymmetry: under `cameras.ffmpeg.inputs` you should use the **raw** password (Frigate encodes it for you) — pre-encoding it there causes a double-encode and fails. See [Handling Complex Passwords](/configuration/restream#handling-complex-passwords). +Note the asymmetry: under `cameras.ffmpeg.inputs` you should use the **raw** password (Frigate encodes it for you). Pre-encoding it there causes a double-encode and fails. See [Handling Complex Passwords](/configuration/restream#handling-complex-passwords). Repeated `401`/`Connection refused` errors can also mean the camera hit its **concurrent connection limit** or triggered a login lockout. Routing all roles through a single [RTSP restream](/configuration/restream#reduce-connections-to-camera) means the camera only ever sees one connection from go2rtc. @@ -180,26 +180,26 @@ A surprising number of "the better live options aren't available" or `404 Not Fo - the **go2rtc stream key** (`go2rtc.streams.`), - any `ffmpeg:#…` source that references it, - the camera's restream input path (`rtsp://127.0.0.1:8554/`), and -- the camera name itself (so Frigate auto-maps it for MSE/WebRTC) — or an explicit `live -> streams` mapping pointing at the go2rtc stream **name** (never a path). +- the camera name itself (so Frigate auto-maps it for MSE/WebRTC), or an explicit `live -> streams` mapping pointing at the go2rtc stream **name** (never a path). -If you rename or remove a go2rtc stream while experimenting and the live stream selector then shows a blank entry, clear your browser's site data for the Frigate URL — the selected stream is cached per-device in local storage. +If you rename or remove a go2rtc stream while experimenting and the live stream selector then shows a blank entry, clear your browser's site data for the Frigate URL. The selected stream is cached per-device in local storage. ## Camera-specific behavior Several camera brands have well-known quirks with go2rtc. Rather than repeat them here, see the [camera-specific configuration](/configuration/camera_specific) page, which covers them in detail. The highlights: -- **Reolink** — RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras). -- **TP-Link Tapo** — use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI. -- **Ubiquiti/UniFi Protect** — use the `rtspx://` scheme (not `rtsps://…?enableSrtp`). -- **Amcrest/Dahua** — use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua). +- **Reolink**: RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras). +- **TP-Link Tapo**: use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI. +- **Ubiquiti/UniFi Protect**: use the `rtspx://` scheme (not `rtsps://…?enableSrtp`). +- **Amcrest/Dahua**: use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua). ## Non-RTSP sources and the FFmpeg module -go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else — MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs — must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example. +go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else (MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs) must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example. ## Hardware-accelerated transcoding with FFmpeg 8 -Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about hardware-accelerated filtering than earlier versions. Whenever go2rtc transcodes video with hardware acceleration (any source using `#hardware`, `#hardware=vaapi`, or the **Automatic** hardware option in the UI), it builds a filter chain that uploads frames to the GPU with the `hwupload` filter. FFmpeg 8 now refuses to do this unless it is told **which device** to use — earlier versions selected one automatically. The result is that an otherwise-working transcode fails to start, the live view never loads, and go2rtc logs: +Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about hardware-accelerated filtering than earlier versions. Whenever go2rtc transcodes video with hardware acceleration (any source using `#hardware`, `#hardware=vaapi`, or the **Automatic** hardware option in the UI), it builds a filter chain that uploads frames to the GPU with the `hwupload` filter. FFmpeg 8 now refuses to do this unless it is told **which device** to use. Earlier versions selected one automatically. The result is that an otherwise-working transcode fails to start, the live view never loads, and go2rtc logs: ``` [hwupload] A hardware device reference is required to upload frames to. @@ -207,7 +207,7 @@ Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about Error opening output files: Invalid argument ``` -The fix is to tell go2rtc's bundled FFmpeg which hardware device to use via the `go2rtc -> ffmpeg -> global` option. For **VAAPI**-based acceleration — which covers most Intel and AMD GPUs, and is what go2rtc selects automatically on that hardware — point it at your render device: +The fix is to tell go2rtc's bundled FFmpeg which hardware device to use via the `go2rtc -> ffmpeg -> global` option. For **VAAPI**-based acceleration (which covers most Intel and AMD GPUs, and is what go2rtc selects automatically on that hardware), point it at your render device: ```yaml go2rtc: @@ -220,7 +220,7 @@ go2rtc: `/dev/dri/renderD128` is the usual render node; on a system with more than one GPU you may need `renderD129` (or higher), and the device must be passed into the container (e.g. `devices: - /dev/dri:/dev/dri` in Docker Compose). -If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples. +If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.14#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples. :::tip @@ -230,6 +230,6 @@ If you don't transcode in go2rtc with hardware acceleration, this does not affec ## Home Assistant and port access -When running Frigate as a Home Assistant add-on, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them — for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC — go to , click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`. +When running Frigate as a Home Assistant App, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them (for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC), go to , click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`. -If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name — name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream. +If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name: name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream. diff --git a/docs/docs/troubleshooting/gpu.md b/docs/docs/troubleshooting/gpu.md index 6399f92d8b..a39386bfa9 100644 --- a/docs/docs/troubleshooting/gpu.md +++ b/docs/docs/troubleshooting/gpu.md @@ -10,4 +10,47 @@ title: GPU Errors Some users have reported issues using some Intel iGPUs with OpenVINO, where the GPU would not be detected. This error can be caused by various problems, so it is important to ensure the configuration is setup correctly. Some solutions users have noted: - In some cases users have noted that an HDMI dummy plug was necessary to be plugged into the motherboard's HDMI port. -- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container. \ No newline at end of file +- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container. + +## Intel/AMD GPU + +### Hardware acceleration is not being used + +For VAAPI or QSV to work, the GPU's render device must be passed through to the Frigate container. Intel and AMD GPUs expose this as a render node under `/dev/dri`, usually `/dev/dri/renderD128`. If it is not passed through, hardware acceleration is unavailable: ffmpeg fails to initialize it (for example `Failed to open the drm device` or `No VA display found for device`) and GPU usage stays at zero while CPU usage remains high. + +Pass the render device through when starting the container. With `docker compose`: + +```yaml +services: + frigate: + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 # Intel / AMD GPU, update for your hardware +``` + +Or with `docker run`, add `--device /dev/dri/renderD128`. See the [installation docs](/frigate/installation) for a complete example. + +If it still isn't working after passing the device through: + +- **Confirm the render node exists and is the correct one.** Run `ls /dev/dri` on the host. You should see one or more `renderD12X` entries. Systems with more than one GPU (an Intel iGPU plus a discrete GPU) can expose both `/dev/dri/renderD128` and `/dev/dri/renderD129`, and the numbering is not guaranteed. Pass through the correct node, or map the entire directory (`/dev/dri:/dev/dri`, or `--device /dev/dri`) so all render nodes are available. +- **Check device permissions.** The Frigate process must be able to access the render node. This is usually automatic when the container runs as root (the default), but nested setups such as an unprivileged Proxmox/LXC container often require making the device accessible on the host (for example, a world-readable render node) or running the container privileged. Note that running Frigate inside an LXC is not officially supported. See the [installation docs](/frigate/installation#proxmox) for details. + +### Failed to download frame: -5 + +When using VAAPI or QSV hardware acceleration, ffmpeg may crash and restart periodically with a signature like this in the `ffmpeg..detect` log: + +``` +[AVHWFramesContext @ 0x...] Failed to sync surface ... (operation failed). +[hwdownload @ 0x...] Failed to download frame: -5. +[vf#0:0 @ 0x...] Error while filtering: Input/output error +[vf#0:0 @ 0x...] Task finished with error code: -5 (Input/output error) +[frigate.video] : Unable to read frames from ffmpeg process. +``` + +This is a hardware frame synchronization failure between ffmpeg and the GPU driver, not a Frigate bug. It comes from how a specific camera stream interacts with the GPU's decode and scaling path, so it is highly dependent on your hardware, driver, and stream. Frigate's automatic hardware acceleration detection is a best-guess effort, so the fix is usually to tune the configuration for your specific hardware and camera. The solutions below are ordered from most to least likely to help: + +- **Switch between the VAAPI and QSV presets.** On Intel Gen 12 and newer iGPUs, `preset-intel-qsv-h264` / `preset-intel-qsv-h265` is often more stable than the auto-detected `preset-vaapi`. See the [hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for the recommended preset for your Intel generation. +- **Try a different VAAPI driver.** The default driver is `iHD`. On older Intel CPUs, `LIBVA_DRIVER_NAME=i965` can be more stable; on AMD GPUs use `LIBVA_DRIVER_NAME=radeonsi`. See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for how to set the driver. +- **Use a codec that decodes more reliably.** H.265/HEVC streams may trigger this error far more often than H.264 depending on your CPU generation. If your camera exposes a separate sub-stream, assign an H.264 stream to the `detect` role. Cameras that output full-range YUV (for example some Hikvision models) are especially prone to it. +- **Match the detect resolution to the stream resolution.** When the `detect` resolution differs from the stream, Frigate inserts a GPU scaling filter (`scale_vaapi`), which is where these surface-sync failures can often originate. Set the `detect` `width` and `height` to match the exact resolution of the stream assigned the `detect` role. +- **Match the detect `fps` to the camera stream.** Aggressively dropping frames (for example `detect` `fps: 1` on a stream that runs at 15 fps) can cause timing mismatches in the GPU's frame buffer. Lower the sub-stream's frame rate on the camera itself instead of dropping most frames in Frigate. +- **Fall back to software decoding.** If none of the above resolve it, remove the preset for that camera (`hwaccel_args: []`). Hardware decoding is only an optimization. On a capable CPU, software-decoding a low-resolution sub-stream is inexpensive and gives a stable detect pipeline. diff --git a/docs/docs/troubleshooting/recordings.md b/docs/docs/troubleshooting/recordings.md index 2425e653a4..c4e8e72b25 100644 --- a/docs/docs/troubleshooting/recordings.md +++ b/docs/docs/troubleshooting/recordings.md @@ -3,17 +3,13 @@ id: recordings title: Recordings Errors --- -## I have Frigate configured for motion recording only, but it still seems to be recording even with no motion. Why? +import FaqItem from "@site/src/components/FaqItem"; -You'll want to: +## Why are my recordings not working? (empty Recordings, "No recordings found for this time") -- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion. -- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion. -- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner. +If Frigate shows live video but the History view is empty, or you see "No recordings found for this time", the cause is almost always in one of the three categories below. Segments are first written to the RAM cache and are only moved to disk if they match a retention policy _and_ the camera's `record` stream is producing valid, storable video. Work through the categories in order: retention configuration is by far the most common cause. -## I see the message: WARNING : Unable to keep up with recording segments in cache for camera. Keeping the 5 most recent segments out of 6 and discarding the rest... - -This error can be caused by a number of different issues. The first step in troubleshooting is to enable debug logging for recording. This will enable logging showing how long it takes for recordings to be moved from RAM cache to the disk. +Before diving in, enable debug logging for the recording maintainer so you can see whether segments are being written to disk at all: ```yaml logger: @@ -21,31 +17,240 @@ logger: frigate.record.maintainer: debug ``` -This will include logs like: +A healthy camera logs lines like `Copied /media/frigate/recordings/{segment_path} in 0.2 seconds`. If you never see these, no segments are reaching disk, which points at the camera/stream or storage sections below. + +### Retention configuration issues + + + +This is the single most common cause. Setting `record.enabled: True` on its own does **not** keep any footage: **continuous recording is disabled by default**, and segments in the cache are only moved to disk if they match a configured retention policy. You must configure at least one of `continuous`, `motion`, `alerts`, or `detections` retention. + +To store all video (the most conservative option), configure continuous retention: + +```yaml +record: + enabled: True + continuous: + days: 3 # keep all footage for 3 days +``` + +See [Recording](/configuration/record) for the full set of common configurations, including reduced-storage and alerts-only setups. + + + + + +If you only configured `motion`, `alerts`, or `detections` retention (with no `continuous`), Frigate keeps footage selectively based on the retention `mode`: + +- **`mode: motion`** (the default) only retains segments that contain motion. If your [motion masks](/configuration/motion_detection) cover the areas where activity happens, or your motion sensitivity is too low, nothing will be retained even though recording is "on". +- **`mode: active_objects`** only retains segments where a tracked object was actively moving. +- **`mode: all`** retains every segment in the window. + +If you expected continuous footage but only configured motion/event retention, add a `continuous` retention period as shown above. To verify motion is actually being detected, watch the motion boxes in the debug view or the Motion Tuner in the UI. + + + + + +`alerts` and `detections` retention only keep footage that overlaps a tracked object, so they depend on object detection running: + +- **Detection must be enabled.** If `detect: enabled: False`, no alerts or detections are ever created, so alert/detection retention keeps nothing. (Continuous and motion retention still work with detection disabled.) +- **The object must be supported by your model.** If you track an object your model doesn't support (for example `deer` or `license_plate` on the default model), Frigate never detects it and never records for it. Check your logs for warnings such as `... is configured to track ['deer'] objects, which are not supported by the current model` and remove unsupported objects or switch to a model (e.g. [Frigate+](/plus/)) that includes them. + + + + + +Configuration keys change between major versions. The old `clips` config, for example, has not existed for a long time. If you copied a config from an old blog post or video, verify every key against the current [reference config](/configuration/advanced/reference). + + + +### Camera and stream issues + + + +Frigate stores recordings in an MP4 container, and some camera audio codecs (most commonly `pcm_alaw`, `pcm_mulaw`, or other G.711 variants) **cannot be placed in an MP4 container**. When this happens, ffmpeg fails to write the segment and no recording is saved, even though the live view works fine. This is a frequent cause on Tapo, TP-Link VIGI, and some Reolink cameras. + +Transcode the audio to AAC (or drop it entirely) using the appropriate [ffmpeg preset](/configuration/ffmpeg_presets): + +```yaml +cameras: + your_camera: + ffmpeg: + output_args: + record: preset-record-generic-audio-aac # transcode audio to AAC + # or preset-record-generic to record with no audio +``` + + + + + +A message like `No new recording segments were created for in the last 120s` means ffmpeg cannot read the `record` stream. To diagnose: + +- Confirm a stream is actually assigned the `record` role in your camera's `ffmpeg.inputs`. +- Open the go2rtc web interface on port `1984` and click each stream to confirm it plays. go2rtc errors such as `wrong response on DESCRIBE` or `start from CONN state` indicate the camera connection is failing. +- Test the exact RTSP URL (with the correct path, port, and credentials) in VLC or `ffplay`. +- If you restream through go2rtc, make sure the `record` input path points at the correct go2rtc stream name. Copying a config between cameras without updating the stream name is a common mistake. + + + +### Storage and mounting issues + + + +If the recordings volume (`/media/frigate`) points at the wrong location, isn't writable, or a network/encrypted mount failed to mount at boot, Frigate cannot save recordings, or it silently writes to the boot drive and then purges aggressively because the drive appears far smaller than expected. + +- Compare the host's real capacity (`df -h`) against what the **Storage** page in the Frigate UI reports. A mismatch (for example Frigate reporting ~220 GB when your storage drive is 4 TB) means the bind mount is resolving to the wrong filesystem. +- Verify the host path in your Docker `volumes` mapping (`- /your/storage:/media/frigate`) exists and is writable by the container. +- For a mount that may fail intermittently, protecting the mount point with `chattr +i` on an empty directory forces Frigate to error out (rather than silently writing to the boot drive) when the mount is missing. +- Check `dmesg` and system logs for filesystem or I/O errors around the time recordings disappeared. + +If recordings _are_ being written but the copy is too slow to keep up, see the ["Unable to keep up with recording segments"](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) question below. + + + +## Recordings won't play back + + + +When a recording refuses to play in the Frigate UI and you see an error like `Failed to play recordings (error 3): PIPELINE_ERROR_DECODE`, the message is coming from **your browser**, not from Frigate. `PIPELINE_ERROR_DECODE` is emitted exclusively by the media pipeline in **Chromium-based browsers** (Chrome, Edge, Brave, Vivaldi, Opera, Arc, and the Android WebView used by many in-app browsers) when the browser cannot decode a video or audio packet in the recording. WebKit browsers (Safari) report the same underlying problem with a different message, usually `Media failed to decode` or `DECODER_ERROR_NOT_SUPPORTED`. + +Frigate copies the `record` stream to disk **without re-encoding it**, so the browser must decode exactly what your camera produced, and Chromium's decoder is far stricter about malformed or nonstandard media than VLC or ffmpeg. + +:::warning + +The same recording playing perfectly in VLC, decoding cleanly with `ffprobe`/`ffmpeg`, or having a valid MP4 container does **not** mean the browser can decode it. VLC and ffmpeg are much more tolerant of codec quirks and damaged packets than a browser's media pipeline, so a "valid" file can still trigger `PIPELINE_ERROR_DECODE`. This is outside of Frigate's control, because Frigate never modifies the recording stream. + +::: + +#### Step 1: Confirm it is a browser issue + +Open the same recording in **Firefox** or **Safari**. Firefox and Safari both use a different media engine and cannot produce `PIPELINE_ERROR_DECODE`, so if playback works there you have confirmed a client-side codec or decoder problem rather than a bad recording. Switching browsers is a workaround, not a fix; the remaining steps address the root cause so that Chromium browsers work too. + +#### Step 2: Rule out H.265 / HEVC + +Browser support for H.265 (HEVC) is limited and depends on the operating system, GPU, hardware acceleration, and browser version, which makes it the most common cause of this error. Options, in order of reliability: + +- **Record H.264 instead.** Configure the camera's `record`/main stream to output H.264, the most compatible codec across all browsers. See [camera settings recommendations](/configuration/live#camera-settings-recommendations). +- **Transcode to H.264 with go2rtc.** If you must keep HEVC on the camera, have go2rtc re-encode the recording stream. This increases CPU usage; add `#hardware` to use the GPU where available: + + ```yaml + go2rtc: + streams: + your_camera: + # transcode video to h264 and audio to aac; #hardware uses the GPU if available + - "ffmpeg:rtsp://user:password@CAMERA_IP:554/stream#video=h264#audio=aac#hardware" + cameras: + your_camera: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/your_camera + input_args: preset-rtsp-restream + roles: + - record + ``` + + The `#video=h264` parameter only takes effect with the `ffmpeg:` source module; adding it to a plain `rtsp://` go2rtc source does nothing. + +- **Keep HEVC but improve compatibility.** If your browser and OS do support HEVC, set [`apple_compatibility`](/configuration/camera_specific#h265-cameras-via-safari) on the camera. Some players (Safari and other clients) require a specific HEVC stream format that this option corrects: + + ```yaml + cameras: + your_camera: + ffmpeg: + apple_compatibility: true + ``` + + You may also need to enable HEVC and hardware decoding in the browser itself (for example, Chrome's Settings → System → "Use hardware acceleration when available"). HEVC hardware support varies widely by GPU, OS, and browser version. + +#### Step 3: Clean up damaged packets from the camera + +If the error is **intermittent** (the same recording plays after a page refresh, or fails only after playing for a while), the camera is most likely emitting occasional corrupt or malformed packets. Some camera models are more prone to this than others. Routing the stream through go2rtc's `ffmpeg` module often "cleans up" the stream enough for the browser to decode it, even without changing the codec: + +```yaml +go2rtc: + streams: + your_camera: + - "ffmpeg:rtsp://user:password@CAMERA_IP:554/stream#video=h264#audio=aac" +``` + +#### Step 4: Fix incompatible or corrupt audio + +Audio is one of the most common culprits, and a decode failure on the audio track fails the whole recording. Make sure the camera outputs **AAC** audio, transcode the audio to AAC with go2rtc (`#audio=aac`), or drop audio entirely. See [Incompatible audio codec](#incompatible-audio-codec-recordings-silently-fail-to-save) for a preset-based way to do this. + +#### Step 5: Avoid "smart" / "+" codecs and check the keyframe interval + +- Disable any **"Smart Codec"**, **"H.264+"**, or **"H.265+"** feature in the camera. These nonstandard modes drop keyframes and change encoding parameters mid-stream, producing exactly the kind of packets a browser refuses to decode. (They also cause [short recording segments](#segments-are-only-1-second-long).) +- Set the camera's **I-frame (keyframe) interval equal to the frame rate** (for example `20` for a 20 fps stream). Long keyframe intervals slow the start of playback and make decode errors more likely. + +#### Step 6: Consider bitrate and the client hardware + +The browser decodes the video locally, so a stream that is too demanding can fail on one device while playing on another: + +- A **very high bitrate or resolution** (for example a 4K/8MP HEVC main stream) can overwhelm a low-power tablet, phone, or SBC and stall the decoder. Test the same recording on a desktop; if it plays there, lower the camera's bitrate or record a lower-resolution profile. +- Errors that name the client's GPU decoder, such as `VaapiVideoDecoder: failed Initialize()ing the frame pool`, indicate a browser hardware-decode problem. Toggling the browser's "Use hardware acceleration" setting (on or off) often resolves these. + + + + + +Frigate copies the `record` stream directly without re-encoding, so playback depends on your browser supporting the camera's codec. H265/HEVC recordings may not be playable in some browsers. If recordings appear as audio-only or a black screen, your camera is likely sending a codec your browser can't decode. Configure the camera to output **H264** for maximum compatibility. + +If playback instead fails with an explicit `PIPELINE_ERROR_DECODE` or `Media failed to decode` error, see [Recordings won't play back with "PIPELINE_ERROR_DECODE"](#pipeline-error-decode) above. + + + +## Recording cache warnings and errors + + + +If the record stream uses a "Smart Codec"/H.264+ mode or changes encoding parameters mid-stream, corrupt timestamps cause segments to be split far too frequently and fill the cache. This produces the "Too many unprocessed recording segments" warning. See [that question below](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) for the full diagnosis. + + + + + +This warning means the recording maintainer cannot move recording segments from the RAM cache to disk fast enough. When the cache fills up, Frigate discards the oldest segments to avoid running out of memory and crashing, so you lose recorded footage. This is almost always a storage throughput or system resource problem. Work through the steps below to identify which. + +#### Step 1: Enable recording debug logging + +The first step is to measure how long each segment takes to move from the RAM cache to disk. Enable debug logging for the recording maintainer: + +```yaml +logger: + logs: + frigate.record.maintainer: debug +``` + +This adds log lines showing the copy duration for each segment: ``` DEBUG : Copied /media/frigate/recordings/{segment_path} in 0.2 seconds. ``` -It is important to let this run until the errors begin to happen, to confirm that there is not a slow down in the disk at the time of the error. +Let this run until the warnings begin to appear, so you can confirm whether the disk is actually slowing down at the moment the error occurs. -#### Copy Times > 1 second +#### Step 2: Interpret the copy times -If the storage is too slow to keep up with the recordings then the maintainer will fall behind and purge the oldest recordings to ensure the cache does not fill up causing a crash. In this case it is important to diagnose why the copy times are slow. +The copy duration tells you which direction to investigate: -##### Check RAM, swap, cache utilization, and disk utilization +- **Consistently longer than ~1 second**: your storage cannot keep up with the incoming recordings. Continue with Steps 3–5 to diagnose the slow storage. +- **Consistently well under 1 second**: storage is fast enough, and the problem is more likely CPU or resource contention. Skip to Step 6. -If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside frigate will help. It is important to review each aspect of available system resources. +#### Step 3: Check RAM, swap, cache, and disk utilization -On linux, some helpful tools/commands in diagnosing would be: +If CPU, RAM, disk throughput, or bus I/O is insufficient, nothing inside Frigate will help. Review each aspect of available system resources while the warnings are occurring. -- docker stats -- htop -- iotop -o -- iostat -sxy --human 1 1 -- vmstat 1 +On Linux, some helpful tools/commands for diagnosing this are: -On modern linux kernels, the system will utilize some swap if enabled. Setting vm.swappiness=1 no longer means that the kernel will only swap in order to avoid OOM. To prevent any swapping inside a container, set allocations memory and memory+swap to be the same and disable swapping by setting the following docker/podman run parameters: +- `docker stats` +- `htop` +- `iotop -o` +- `iostat -sxy --human 1 1` +- `vmstat 1` + +On modern Linux kernels, the system will use some swap if it is enabled. Setting `vm.swappiness=1` no longer means the kernel will only swap in order to avoid OOM. To prevent any swapping inside the container, set the memory and memory+swap allocations to the same value and disable swapping by setting the following docker/podman run parameters: **Docker Compose example** @@ -67,23 +272,25 @@ services: --memory= --memory-swap= --memory-swappiness=0 ``` -NOTE: These are hard-limits for the container, be sure there is enough headroom above what is shown by `docker stats` for your container. It will immediately halt if it hits ``. In general, running all cache and tmp filespace in RAM is preferable to disk I/O where possible. +NOTE: These are hard limits for the container, so be sure there is enough headroom above what `docker stats` shows for your container. It will immediately halt if it hits ``. In general, keeping all cache and tmp filespace in RAM is preferable to disk I/O where possible. -##### Check Storage Type +#### Step 4: Check your storage type -Mounting a network share is a popular option for storing Recordings, but this can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased the copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. +Mounting a network share is a popular option for storing recordings, but it can lead to reduced copy times and cause problems. Some users have found that using `NFS` instead of `SMB` considerably decreased copy times and fixed the issue. It is also important to ensure that the network connection between the device running Frigate and the network share is stable and fast. A saturated or unreliable link will stall copies. -##### Check mount options +#### Step 5: Check your mount options -Some users found that mounting a drive via `fstab` with the `sync` option caused dramatically reduce performance and led to this issue. Using `async` instead greatly reduced copy times. +Some users found that mounting a drive via `fstab` with the `sync` option dramatically reduced performance and led to this issue. Using `async` instead greatly reduced copy times. -#### Copy Times < 1 second +#### Step 6: Rule out CPU load -If the storage is working quickly then this error may be caused by CPU load on the machine being too high for Frigate to have the resources to keep up. Try temporarily shutting down other services to see if the issue improves. +If the copy times are consistently under 1 second but you still see the warning, the machine's CPU load is likely too high for Frigate to have the resources to keep up. Try temporarily shutting down other services, and any resource-intensive Frigate features, to see if the issue improves. -## I see the message: WARNING : Too many unprocessed recording segments in cache for camera. This likely indicates an issue with the detect stream... + -This warning means that the detect stream for the affected camera has fallen behind or stopped processing frames. Frigate's recording cache holds segments waiting to be analyzed by the detector — when more than 6 segments pile up without being processed, Frigate discards the oldest ones to prevent the cache from filling up. + + +This warning means that the detect stream for the affected camera has fallen behind or stopped processing frames. Frigate's recording cache holds segments waiting to be analyzed by the detector. When more than 6 segments pile up without being processed, Frigate discards the oldest ones to prevent the cache from filling up. :::warning @@ -91,11 +298,11 @@ This error is a **symptom**, not the root cause. The actual cause is always logg ::: -### Step 1: Get the full logs +#### Step 1: Get the full logs -Collect complete Frigate logs from startup through the first occurrence of the error. Look for errors or warnings that appear **before** the "Too many unprocessed" messages begin — that is where the root cause will be found. +Collect complete Frigate logs from startup through the first occurrence of the error. Look for errors or warnings that appear **before** the "Too many unprocessed" messages begin. That is where the root cause will be found. -### Step 2: Check the cache directory +#### Step 2: Check the cache directory Exec into the Frigate container and inspect the recording cache: @@ -105,7 +312,7 @@ docker exec -it frigate ls -la /tmp/cache Each camera should have a small number of `.mp4` segment files. If one camera has significantly more files than others, that camera is the source of the problem. A problem with a single camera can cascade and cause all cameras to show this error. -### Step 3: Verify segment duration +#### Step 3: Verify segment duration Recording segments should be approximately 10 seconds long. Run `ffprobe` on segments in the cache to check: @@ -117,20 +324,26 @@ If segments are only ~1 second instead of ~10 seconds, the camera is sending cor **Common causes of short segments:** -- **"Smart Codec" or "Smart+" enabled on the camera** — These features dynamically change encoding parameters mid-stream, which corrupts timestamps. Disable them in your camera's settings. -- **Changing codec, bitrate, or resolution mid-stream** — Any encoding changes during an active stream can cause unpredictable segment splitting. -- **Camera firmware bugs** — Check for firmware updates from your camera manufacturer. +- **"Smart Codec" or "Smart+" enabled on the camera**: These features dynamically change encoding parameters mid-stream, which corrupts timestamps. Disable them in your camera's settings. +- **Changing codec, bitrate, or resolution mid-stream**: Any encoding changes during an active stream can cause unpredictable segment splitting. +- **Camera firmware bugs**: Check for firmware updates from your camera manufacturer. -### Step 4: Check for a stuck detector +:::tip + +You don't have to run `ffprobe` by hand to catch this. Open a camera's **Camera Probe Info** dialog (the info icon on the System → Metrics → Cameras page) and check the **Keyframe analysis** section. It probes the record stream and flags sparse or variable keyframes, which is what smart/"+" codecs (H.264+/H.265+) and long keyframe intervals produce. + +::: + +#### Step 4: Check for a stuck detector If the detect stream is not processing frames, segments will accumulate. Common causes: -- **Detection resolution too high** — Use a substream for detection, not the full resolution main stream. -- **Detection FPS too high** — 5 fps is the recommended maximum for detection. -- **Model too large** — Use smaller model variants (e.g., YOLO `s` or `t` size, not `e` or `x`). Use 320x320 input size rather than 640x640 unless you have a powerful dedicated detector. -- **Virtualization** — Running Frigate in a VM (especially Proxmox) can cause the detector to hang or stall. This is a known issue with GPU/TPU passthrough in virtualized environments and is not something Frigate can fix. Running Frigate in Docker on bare metal is recommended. +- **Detection resolution too high**: Use a substream for detection, not the full resolution main stream. +- **Detection FPS too high**: 5 fps is the recommended maximum for detection. +- **Model too large**: Use smaller model variants (e.g., YOLO `s` or `t` size, not `e` or `x`). Use 320x320 input size rather than 640x640 unless you have a powerful dedicated detector. +- **Virtualization**: Running Frigate in a VM (especially Proxmox) can cause the detector to hang or stall. This is a known issue with GPU/TPU passthrough in virtualized environments and is not something Frigate can fix. Running Frigate in Docker on bare metal is recommended. -### Step 5: Check for GPU hangs +#### Step 5: Check for GPU hangs On the host machine, check `dmesg` for GPU-related errors: @@ -140,25 +353,78 @@ dmesg | grep -i -E "gpu|drm|reset|hang" Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date. -### Step 6: Verify hardware acceleration configuration +#### Step 6: Verify hardware acceleration configuration An incorrect `hwaccel_args` preset can cause ffmpeg to fail silently or consume excessive CPU, starving the detector of resources. - After upgrading Frigate, verify your preset matches your hardware (e.g., `preset-intel-qsv-h264` instead of the deprecated `preset-vaapi`). - For h265 cameras, use the corresponding h265 preset (e.g., `preset-intel-qsv-h265`). -- Note that `hwaccel_args` are only relevant for the detect stream — Frigate does not decode the record stream. +- Note that `hwaccel_args` are only relevant for the detect stream. Frigate does not decode the record stream. -### Step 7: Verify go2rtc stream configuration +#### Step 7: Verify go2rtc stream configuration Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely. -### Step 8: Check system resources +#### Step 8: Check system resources If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host: -- **CPU usage** — An overloaded CPU can prevent the detector from keeping up. -- **RAM and swap** — Excessive swapping dramatically slows all I/O operations. -- **Disk I/O** — Use `iotop` or `iostat` to check for saturation. -- **Storage space** — Verify you have free space on the Frigate storage volume (check the Storage page in the Frigate UI). +- **CPU usage**: An overloaded CPU can prevent the detector from keeping up. +- **RAM and swap**: Excessive swapping dramatically slows all I/O operations. +- **Disk I/O**: Use `iotop` or `iostat` to check for saturation. +- **Storage space**: Verify you have free space on the Frigate storage volume (check the Storage page in the Frigate UI). Try temporarily disabling resource-intensive features like `genai` and `face_recognition` to see if the issue resolves. This can help isolate whether the detector is being starved of resources. + + + + + +This message means the recording maintainer hit an error while moving segments from the cache to disk. It is a **generic wrapper**: the actual cause is always logged on the **very next line**. Frigate usually recovers and keeps running, but any affected segments are lost, so it is worth resolving. + +:::warning + +Always read the line immediately following this message. `Error occurred when attempting to maintain recording cache` on its own tells you nothing; the exception on the next line (for example `[Errno 28] No space left on device` or `[Errno 17] File exists`) is the real problem. + +::: + +Because these are operating-system-level errors, they must be resolved on the **host**, not within Frigate's configuration. The most common underlying errors are below. + +#### [Errno 28] No space left on device + +The filesystem Frigate is writing to is full. Things to check: + +- **The recordings volume is genuinely full.** Check free space on the host with `df -h` for the path mapped to `/media/frigate`, and review the **Storage** page in the Frigate UI. +- **The disk shows free space but is still "full".** This usually means the filesystem has run out of **inodes** (check with `df -i`), or recordings are landing on a different, smaller filesystem than you expect because of an incorrect bind mount. See [The storage volume isn't mounted correctly](#the-storage-volume-isnt-mounted-correctly) above. +- **`/tmp/cache` is full.** If you mounted `/tmp/cache` as a small `tmpfs`, a backlog of segments can fill it. Increase the tmpfs size, or address whatever is causing segments to pile up (see the [Too many unprocessed recording segments](#i-see-the-message-warning--too-many-unprocessed-recording-segments-in-cache-for-camera-this-likely-indicates-an-issue-with-the-detect-stream) question above). +- **The host blocks writes before Frigate can purge.** On some systems (for example Unraid with a fill-up threshold), the host stops writes before Frigate's emergency cleanup can run. Leave more headroom on the volume, or lower your retention so Frigate purges sooner. + +#### [Errno 17] File exists (with ffmpeg "Error writing trailer" or "unable to re-open output file") + +Errors like `[Errno 17] File exists: '/media/frigate/recordings/.../'`, often alongside ffmpeg errors such as `Unable to re-open ... output file for shifting data` or `Error writing trailer: No such file or directory`, are a hallmark of an **unreliable network share** (NFS or SMB). The mount is dropping, serving stale directory entries, or mishandling file locking. + +- Confirm the network connection to the NAS is stable and fast. An intermittent link produces these errors sporadically. +- Prefer **NFS over SMB** for the recordings mount; several users have found NFS more reliable and faster. +- Review your `fstab`/mount options for settings that hurt consistency or performance (see the `sync` vs `async` note in the [Unable to keep up with recording segments](#i-see-the-message-warning--unable-to-keep-up-with-recording-segments-in-cache-for-camera-keeping-the-5-most-recent-segments-out-of-6-and-discarding-the-rest) question above). +- Enable `frigate.record.maintainer` debug logging to confirm whether the errors line up with the share becoming unavailable. + +#### Errors referencing a camera you manually renamed or removed + +If the next-line error references a camera name that no longer exists in your config, orphaned data is left over from a rename or removal in a persistent `/tmp/cache` volume. + +- Using a `tmpfs` mount for `/tmp/cache` as recommended in the [installation docs](/frigate/installation#storage) prevents stale cache files under the old camera name from surviving a restart, which avoids this issue entirely. +- If errors persist, stop Frigate and remove any leftover segments for the old camera name from `/tmp/cache`. + + + +## Other recording questions + + + +You'll want to: + +- Make sure your camera's timestamp is masked out with a motion mask. Even if there is no motion occurring in your scene, your motion settings may be sensitive enough to count your timestamp as motion. +- If you have audio detection enabled, keep in mind that audio that is heard above `min_volume` is considered motion. +- [Tune your motion detection settings](/configuration/motion_detection) either by editing your config file or by using the UI's Motion Tuner. + + diff --git a/docs/docs/usage/explore.md b/docs/docs/usage/explore.md index a371c79979..49cdc82023 100644 --- a/docs/docs/usage/explore.md +++ b/docs/docs/usage/explore.md @@ -9,9 +9,15 @@ 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. +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. Clicking a thumbnail opens its [detail dialog](#tracked-object-details); right-clicking or long-pressing a thumbnail opens an [actions menu](#actions-and-bulk-selection). You can switch to a denser grid layout and adjust the number of columns from the view's settings. @@ -19,8 +25,8 @@ Clicking a thumbnail opens its [detail dialog](#tracked-object-details); right-c When [Semantic Search](/configuration/semantic_search) is enabled, a search bar appears that combines two things in one input: -- **Natural-language search** — type a free-text query and press Enter to run a semantic search over your tracked objects. -- **Filter tokens** — type a `key:` to get suggestions, then a value, to add a structured filter. Each filter becomes a removable chip, and you can chain several together. +- **Natural-language search**: type a free-text query and press Enter to run a semantic search over your tracked objects. +- **Filter tokens**: type a `key:` to get suggestions, then a value, to add a structured filter. Each filter becomes a removable chip, and you can chain several together. You can save a search with the star icon and reload it later, and clear everything with the clear-search icon. A help popover explains the token syntax, for example: @@ -62,12 +68,12 @@ Natural-language search, thumbnail search, and description search all require [S Selecting an object opens the **Tracked Object Details** dialog. Use the arrows (or the left/right keys) to step to the previous or next object. The dialog has two tabs: -- **Snapshot** or **Thumbnail** — the saved snapshot (or thumbnail). -- **Tracking Details** — the object's lifecycle, available when the object has a recording. It lists each significant moment (detected, entered a zone, became active or stationary, left, and so on); clicking a moment plays that part of the recording with the bounding box overlaid. A settings popover lets you show all zones and adjust the annotation offset. +- **Snapshot** or **Thumbnail**: the saved snapshot (or thumbnail). +- **Tracking Details**: the object's lifecycle, available when the object has a recording. It lists each significant moment (detected, entered a zone, became active or stationary, left, and so on); clicking a moment plays that part of the recording with the bounding box overlaid. A settings popover lets you show all zones and adjust the annotation offset. The details pane shows the object's **label**, **scores**, **camera**, **timestamp**, estimated **speed**, any **recognized license plate** and **classification attributes**, and its **description**. Admins can edit the sub label, license plate, and attributes inline. -The **description** can be edited by hand, and — when [Generative AI descriptions](/configuration/genai/genai_objects) are enabled and the object's lifecycle has ended — regenerated from the snapshot or from thumbnails. For `speech` objects, a **Transcribe** action is available when audio transcription is enabled. When [Frigate+](/integrations/plus) is enabled, admins can submit a snapshot to improve their model directly from this pane. +The **description** can be edited by hand, and, when [Generative AI descriptions](/configuration/genai/genai_objects) are enabled and the object's lifecycle has ended, regenerated from the snapshot or from thumbnails. For `speech` objects, a **Transcribe** action is available when audio transcription is enabled. When [Frigate+](/integrations/plus) is enabled, admins can submit a snapshot to improve their model directly from this pane. ## Actions and bulk selection @@ -85,7 +91,7 @@ To act on many objects at once, Ctrl/Cmd-click or right-click to start a selecti 1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results. 2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object. -3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the the ones without them are more relevant. +3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the ones without them are more relevant. 4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day". 5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well. 6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you. diff --git a/docs/docs/usage/exports.md b/docs/docs/usage/exports.md index 91b901c78c..a593374b35 100644 --- a/docs/docs/usage/exports.md +++ b/docs/docs/usage/exports.md @@ -5,7 +5,7 @@ title: Exports **Exports** are how you keep a specific piece of footage permanently. -Frigate's recordings are governed by your [retention settings](/configuration/record): once footage ages past its retention window — or, depending on your configuration, once it is only kept where motion, alerts, or detections occurred — it is deleted to free up disk space. An **export** saves a copy of a chosen time range to a separate location that is **never removed by retention**, so it stays available until you delete it yourself. +Frigate's recordings are governed by your [retention settings](/configuration/record): once footage ages past its retention window (or, depending on your configuration, once it is only kept where motion, alerts, or detections occurred), it is deleted to free up disk space. An **export** saves a copy of a chosen time range to a separate location that is **never removed by retention**, so it stays available until you delete it yourself. This is the answer to the common question _"how do I stop Frigate from deleting an important clip?"_ Instead of increasing retention for an entire camera (which uses far more storage to protect a single moment), export just the footage you want to keep. @@ -19,8 +19,8 @@ Exports are stored under `/media/frigate/exports`, separate from your recordings There are a few ways to create an export: -- **From Review** — select (right click or long-press) an individual review item directly, and choose Export from the header menu. You can also select multiple review items and export them all at once, optionally grouping them into a [case](#cases). -- **From History** — open the **Actions** menu and choose **Export**. You can export a preset duration (the last 1, 4, 8, 12, or 24 hours), enter a custom start and end time, or select a range directly on the timeline. A **multi-camera** option lets you export the same time range across several cameras at once. +- **From Review**: select (right click or long-press) an individual review item directly, and choose Export from the header menu. You can also select multiple review items and export them all at once, optionally grouping them into a [case](#cases). +- **From History**: open the **Actions** menu and choose **Export**. You can export a preset duration (the last 1, 4, 8, 12, or 24 hours), enter a custom start and end time, or select a range directly on the timeline. A **multi-camera** option lets you export the same time range across several cameras at once. In every case you can give the export a name. Frigate then saves the footage from your recordings as a single video file. Larger ranges take time to process; the export is marked _in progress_ until it finishes, and you can keep using Frigate while it runs. @@ -30,14 +30,14 @@ All of your exports live on the **Exports** page, reachable from the main naviga - **Play** it in the browser, - **Download** it to save the footage outside of Frigate, -- **Share** it — copies a direct link to the export (or uses your device's share sheet), +- **Share** it: copies a direct link to the export (or uses your device's share sheet), - **Rename** it, and -- **Delete** it — deleting is the only way an export is removed. +- **Delete** it: deleting is the only way an export is removed. You can also select multiple exports at once to **delete** them in bulk, or to **add them to** (or **remove them from**) a [case](#cases). ## Cases -A **case** groups related exports together — for example, all the clips from a single incident across multiple cameras. On the **Exports** page you can create a case with a name and description, add existing exports to it (or create a new case while exporting), and **download the entire case as a single archive** to hand off as one package. +A **case** groups related exports together: for example, all the clips from a single incident across multiple cameras. On the **Exports** page you can create a case with a name and description, add existing exports to it (or create a new case while exporting), and **download the entire case as a single archive** to hand off as one package. Exports that don't belong to a case appear under **Uncategorized Exports**. Deleting a case lets you either keep its exports (they move back to uncategorized) or delete them along with the case. diff --git a/docs/docs/usage/history.md b/docs/docs/usage/history.md index 457f9f951b..5ed99f440a 100644 --- a/docs/docs/usage/history.md +++ b/docs/docs/usage/history.md @@ -5,7 +5,7 @@ title: History import NavPath from "@site/src/components/NavPath"; -**History** is Frigate's full-resolution recording viewer. Unlike Live, Review, and Explore, there is no menu item for it — you reach it from within another view, then scrub the timeline, switch cameras, inspect a tracked object's lifecycle, and export or share any moment. +**History** is Frigate's full-resolution recording viewer. Unlike Live, Review, and Explore, there is no menu item for it. You reach it from within another view, then scrub the timeline, switch cameras, inspect a tracked object's lifecycle, and export or share any moment. This page describes how to _use_ the History view. For how recordings are _configured_ (retention, pre/post capture), see [Recording](/configuration/record). @@ -19,13 +19,19 @@ You can open History from several places: Use the **Back** button to return where you came from, or the **Live** button to jump to the current camera's live view. +:::tip + +If you see **"No recordings found for this time"**, the most common causes are: recording was not enabled for that camera at the time of the event; the retention window has since expired and those segments were removed; or storage ran low and Frigate deleted them early to free space. See [Recording](/configuration/record) to verify your retention settings. + +::: + ## Timeline, Events, and Detail A toggle (a drawer on mobile) switches the side panel between three modes: -- **Timeline** — a scrubbable vertical timeline of the selected camera, annotated with a motion line, review-item markers, and gaps where no recording exists. -- **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. +- **Timeline**: a scrubbable vertical timeline of the selected camera. Horizontal lines down the center represent motion, with longer lines indicating more motion at that moment. Review items are marked as shaded areas (**red** for alerts, **orange** for detections), and sections with no colored background are times when no recording exists. +- **Events**: a scrollable list of the camera's review items for the time range; clicking one seeks the player to it. +- **Detail**: the [tracking details inspector](#the-detail-view) for the objects in view. While you are selecting a range to export, the panel temporarily switches to Timeline. @@ -43,18 +49,18 @@ You can filter History by **cameras** and **date**. The calendar behaves the sam The **Detail** mode turns the side panel into a tracking details inspector. It lists one card per review item, each showing the item's severity, start time, the object labels involved, a count of tracked objects, and the duration. The active card is highlighted as the video plays, and clicking a card seeks to it. -Expanding a card reveals the **lifecycle** of each tracked object — a row for each significant moment (detected, entered a zone, became active, became stationary, left, and so on), with a progress line that follows the current playback position. Hovering a row shows that moment's score, ratio, and area, and clicking a row seeks the video to that exact timestamp. +Expanding a card reveals the **lifecycle** of each tracked object: a row for each significant moment (detected, entered a zone, became active, became stationary, left, and so on), with a progress line that follows the current playback position. Hovering a row shows that moment's score, ratio, and area, and clicking a row seeks the video to that exact timestamp. -The **Detail View Settings** at the bottom let you toggle whether the active item's objects expand automatically, and adjust the **annotation offset** — a fine timing correction that aligns the bounding-box overlays with the recorded video when your camera's snapshot and recording timestamps drift. Admins can save the offset to the camera's configuration. +The **Detail View Settings** at the bottom let you toggle whether the active item's objects expand automatically, and adjust the **annotation offset**: a fine timing correction that aligns the bounding-box overlays with the recorded video when your camera's snapshot and recording timestamps drift. Admins can save the offset to the camera's configuration. ## The Actions menu On desktop, the **Actions** menu (the film icon) collects the things you can do with the footage you are viewing: -- **Export** — save a clip of a chosen time range so it is never removed by retention. The dialog pre-selects the last hour; adjust the range or drag the timeline handles, then export. See [Exports](/usage/exports) for managing and downloading exports. -- **Share Timestamp** — generate a link to the current moment (or a custom timestamp) to share with another Frigate user. This is an internal link, not a public share URL. -- **Motion Search** — scan this camera's recordings for changes in a region you draw. This is the same tool documented under [Reviewing Motion](/usage/review#motion-search). -- **Debug Replay** (admins) — replay a recorded range back through Frigate's detection pipeline to see how it would be processed. +- **Export**: save a clip of a chosen time range so it is never removed by retention. The dialog pre-selects the last hour; adjust the range or drag the timeline handles, then export. See [Exports](/usage/exports) for managing and downloading exports. +- **Share Timestamp**: generate a link to the current moment (or a custom timestamp) to share with another Frigate user. This is an internal link, not a public share URL. +- **Motion Search**: scan this camera's recordings for changes in a region you draw. This is the same tool documented under [Reviewing Motion](/usage/review#motion-search). +- **Debug Replay** (admins): replay a recorded range back through Frigate's detection pipeline to see how it would be processed. You can also capture an instant snapshot of the current frame, and submit a frame to [Frigate+](/integrations/plus) directly from the player (admins only). @@ -62,8 +68,8 @@ You can also capture an instant snapshot of the current frame, and submit a fram When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them as you scrub through History. A review item that has an AI summary exposes its details in a few places: -- **Over the video** — when the item is on screen, a popup appears over the player. -- **In the Events side panel** — items with a summary show the title below the thumbnail. -- **In the Detail side panel** — the item's card shows the title alongside its tracking details. +- **Over the video**: when the item is on screen, a popup appears over the player. +- **In the Events side panel**: items with a summary show the title below the thumbnail. +- **In the Detail side panel**: the item's card shows the title alongside its tracking details. Clicking any of these opens the **AI Analysis** dialog with the generated detail and any flagged concerns for that item. diff --git a/docs/docs/usage/live.md b/docs/docs/usage/live.md index 2cd3da01ed..9366add9de 100644 --- a/docs/docs/usage/live.md +++ b/docs/docs/usage/live.md @@ -7,13 +7,13 @@ import NavPath from "@site/src/components/NavPath"; **Live view** is Frigate's real-time dashboard and the page you land on by default. It shows all of your cameras at a glance, streams your most recent alerts across the top, and lets you open any camera in a full-resolution single-camera view with audio, two-way talk, PTZ, and on-demand recording controls. -This page describes how to _use_ the Live view. For how to _configure_ live streaming — go2rtc, stream selection, smart streaming, WebRTC, and audio — see the [Live View configuration](/configuration/live) docs. +This page describes how to _use_ the Live view. For how to _configure_ live streaming (go2rtc, stream selection, smart streaming, WebRTC, and audio), see the [Live View configuration](/configuration/live) docs. ## The dashboard at a glance -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. +The default **All Cameras** dashboard shows every camera, with a filmstrip of recent **alerts** scrolling across the top. Clicking an alert opens it in [Review](/usage/review); each card also has a check button to mark it reviewed without leaving the dashboard. Only **alerts** appear in the filmstrip. To suppress a label or zone from showing there, configure it as a detection instead (see [Alerts and Detections](/configuration/review#alerts-and-detections)). -By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this per camera or per group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies). +By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this for each camera when using a camera group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies). On mobile, a toggle in the header switches between a **grid** layout and a single-column **list** layout. On desktop a **fullscreen** button is available in the lower-right corner. @@ -24,7 +24,7 @@ The icon rail (top-left on desktop, a horizontal strip on mobile) switches betwe - The **home** icon is the **All Cameras** dashboard, which shows every camera enabled for the dashboard. - Each **camera group** you create appears as its own icon. Selecting a group shows only that group's cameras. -Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and streaming preferences. +Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and camera streaming preferences. You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open it directly at `http://:5000/#birdseye`. Clicking a camera inside the Birdseye view jumps to that camera's live feed. @@ -33,7 +33,7 @@ You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open Admins can manage groups from the pencil icon next to the group rail, which opens the **Camera Groups** dialog. From there you can add a group, or edit and delete existing ones. When creating a group you choose: - a **Name** (spaces are converted to underscores), -- the **cameras** to include — each camera has a toggle and a gear that opens its [streaming settings](#streaming-settings-and-the-right-click-menu), and +- the **cameras** to include (each camera has a toggle and a gear that opens its [streaming settings](#streaming-settings-and-the-right-click-menu)), and - an **icon** used for the group's button in the rail. Deleting a group also clears any custom layout you saved for it. @@ -42,7 +42,7 @@ Deleting a group also clears any custom layout you saved for it. On desktop and tablet, each camera group has its own freely-arrangeable grid. Enter **Edit Layout** mode from the layout button in the lower-right corner: camera tiles gain a drag handle and corner resize handles. Drag a tile to reposition it and drag a corner to resize it (the aspect ratio is preserved). Exit edit mode to save. The layout is stored in your browser per device, so each device can have its own arrangement. -The default **All Cameras** dashboard is not manually arrangeable — it automatically sizes tiles based on each camera's aspect ratio (wide cameras span two columns, tall cameras span two rows). +The default **All Cameras** dashboard is not manually arrangeable. It automatically sizes tiles based on each camera's aspect ratio (wide cameras span two columns, tall cameras span two rows). ## Reading the tile indicators @@ -58,14 +58,14 @@ You can optionally overlay live streaming statistics (stream type, bandwidth, la ## Streaming settings and the right-click menu -Right-clicking (or long-pressing) a camera tile opens a context menu with quick controls: an **audio volume** control for streams that support audio, **Mute / Unmute all cameras**, **show or hide streaming statistics**, the **debug view**, **notification** options, and — for admins — turning the camera on or off. +Right-clicking (or long-pressing) a camera tile opens a context menu with quick controls: an **audio volume** control for streams that support audio, **Mute / Unmute all cameras**, **show or hide streaming statistics**, the **debug view**, **notification** options, and, for admins, turning the camera on or off. If the audio control doesn't appear, see [Audio Support](/configuration/live#audio-support). Audio requires go2rtc configured with a compatible codec. -A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option appears when Frigate has fallen back to the lower-quality jsmpeg stream — see the [Live view FAQ](/configuration/live#live-view-faq) for why this happens. +A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option when Frigate has fallen back to the lower-quality jsmpeg stream. See the [Live view FAQ](/configuration/live#live-view-faq) for why this happens. For non-default groups, the context menu also exposes **Streaming Settings** for that camera, which let you choose: - the **stream** to display (the dropdown lists the streams you configured under [`live -> streams`](/configuration/live#setting-streams-for-live-ui), and indicates whether audio is available), -- the **streaming method** — **No Streaming**, **Smart Streaming** (recommended), or **Continuous Streaming** (higher bandwidth), and +- the **streaming method**: **No Streaming**, **Smart Streaming** (recommended), or **Continuous Streaming** (higher bandwidth), and - **compatibility mode**, for devices that have trouble rendering the default player. These settings are saved per group and per device in your browser, not in your config file. @@ -77,7 +77,7 @@ Clicking a camera tile opens its full-resolution single-camera view. The top bar - **Back** (also the `Esc` key) to return to the dashboard, - **History** to jump to the [recordings](/usage/history) for this camera, starting about 30 seconds in the past, - **Fullscreen** and **Picture-in-Picture** (if supported by your browser), -- **Two-way talk** (the microphone button — requires a supported camera and WebRTC; keyboard shortcut `t`), and +- **Two-way talk** (the microphone button, which requires a supported camera and WebRTC; keyboard shortcut `t`), and - **Camera audio muting** (the speaker button; keyboard shortcut `m`). You can pinch or scroll to zoom into the feed. A **settings** gear provides a **stream** selector (with audio and two-way-talk availability indicators), **Play in background**, **Show stats**, and a **Debug view** that overlays Frigate's detection regions and bounding boxes. @@ -100,7 +100,7 @@ Admins get a row of toggles in the single-camera view (a settings drawer on mobi - **Live audio transcription** (when audio detection is enabled), and - **Autotracking** (for [autotracking-capable PTZ cameras](/configuration/autotracking)). -These toggles change runtime behavior immediately. Whether a change persists across a restart depends on the feature — see the relevant configuration page. +These toggles change runtime behavior immediately. Whether a change persists across a restart depends on the feature. See the relevant configuration page. ## On-demand recording and snapshots diff --git a/docs/docs/usage/review.md b/docs/docs/usage/review.md index 6a5f05b265..6416b500e1 100644 --- a/docs/docs/usage/review.md +++ b/docs/docs/usage/review.md @@ -5,13 +5,13 @@ title: Review import NavPath from "@site/src/components/NavPath"; -**Review** is where you triage what happened on your cameras. It groups activity into **review items** — segments of time on a single camera that bundle together the objects and audio that were active at once — and sorts them into **Alerts**, **Detections**, and **Motion**. From here you can scrub through activity, mark items as reviewed, filter, export, and jump to the full recording in [History](/usage/history). +**Review** is where you triage what happened on your cameras. It groups activity into **review items**, segments of time on a single camera that bundle together the objects and audio that were active at once, and sorts them into **Alerts**, **Detections**, and **Motion**. From here you can scrub through activity, mark items as reviewed, filter, export, and jump to the full recording in [History](/usage/history). This page describes how to _use_ the Review view. For how alerts and detections are _configured_ (labels, zones, required zones, retention), see the [Review configuration](/configuration/review) docs. :::info -Review items are only created for a camera when **recording is enabled** for that camera. See [Recording](/configuration/record). +Review items are only created for a camera when **object tracking and recording are enabled** for that camera. See [Recording](/configuration/record). ::: @@ -27,7 +27,7 @@ The toggle at the top of the page switches between these three severities. One i | **Detections** | orange | Everything else Frigate tracked that wasn't promoted to an alert. | | **Motion** | yellow | Periods of significant motion, with the ability to filter to periods which did **not** produce a tracked object. | -This same color coding is used for the ring around a selected item and the dots on the calendar. How an object is categorized as an alert vs. a detection — and how required zones refine that — is covered in [Alerts and Detections](/configuration/review#alerts-and-detections). +This same color coding is used for the ring around a selected item and the dots on the calendar. How an object is categorized as an alert vs. a detection, and how required zones refine that, is covered in [Alerts and Detections](/configuration/review#alerts-and-detections). The **Alerts** and **Detections** tabs show a count next to their label. With **Show Reviewed** turned off (the default), this is the number of items still left to review; with it on, the count reflects every item in the selected time range. @@ -39,7 +39,7 @@ Review items are shown as a grid of thumbnail cards next to a vertical activity - The object chip on each card is **gray** when the item is unreviewed and turns **green** once it has been reviewed. - The **Mark these items as reviewed** button marks everything currently shown as reviewed at once. -Reviewed state is tracked per user, so marking an item reviewed does not hide it for other users. +Reviewed state is tracked per user, so marking an item reviewed does not hide it for other users. Marking an item reviewed does not delete anything: the footage and the review item itself remain until they expire via retention. ## Selecting and acting on multiple items @@ -58,13 +58,13 @@ Use the filter controls in the header to narrow what's shown. The available filt The **calendar** filter lets you jump to a specific day (it shows **Last 24 Hours** until you pick one). On each day: - An **underline** under the day number means **recordings exist** for that day. Days without recordings are dimmed. -- A **colored dot** under the day number means there is **unreviewed activity** that day — a **red dot** for unreviewed alerts, or an **orange dot** for unreviewed detections when there are no unreviewed alerts. Motion is not represented by a dot. +- A **colored dot** under the day number means there is **unreviewed activity** that day: a **red dot** for unreviewed alerts, or an **orange dot** for unreviewed detections when there are no unreviewed alerts. Motion is not represented by a dot. Future dates are disabled, and the week start and time zone follow your configuration. ## Reviewing Motion -The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above — see [Tuning Motion Detection](/configuration/motion_detection) for how the underlying motion detector is configured. +The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above. See [Tuning Motion Detection](/configuration/motion_detection) for how the underlying motion detector is configured. The **Motion** tab itself shows a multi-camera grid scrubbed to a shared point in time, with a draggable timeline and a playback-speed selector. A camera tile gains a colored ring when a review item or significant motion overlaps the current time, and clicking a tile opens that camera's recording at that moment. Each camera's options menu (the kebab in the corner of its tile) is where you open **Motion Previews** and **Motion Search**, described below. @@ -76,9 +76,9 @@ On the page, click the kebab menu on a camera The pane provides a few controls: -- **Speed** — speeds up or slows down all of the preview clips at once. -- **Dim** — controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility. -- **Filter** — opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway. +- **Speed**: speeds up or slows down all of the preview clips at once. +- **Dim**: controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility. +- **Filter**: opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway. Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage. @@ -107,20 +107,20 @@ The results panel shows the time range being scanned, a live progress bar with t #### Common use cases -Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing — there is no object to find in Explore, but you suspect something happened. +Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing: there is no object to find in Explore, but you suspect something happened. -- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage — a package now gone, a gate left open — but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand. +- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage (a package now gone, a gate left open), but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand. - **An object that was never detected.** Something Frigate doesn't have a model label for, an object too small or distant to be detected, or movement in a region where detection isn't running. The activity left no tracked object but did change the pixels, so a search can still find it. - **Activity while detection was effectively paused.** Changes that occurred while object detection was disabled, motion was suppressed by `skip_motion_threshold`, or inside an area covered by a motion mask, won't appear as review items or tracked objects but can be recovered by searching the recordings directly. #### Examples -These show how to choose the ROI and **Minimum Change Area** for two common goals. Minimum Change Area is the size of a single moving region as a percentage of the ROI you draw, so the right value depends on how much of the ROI your subject — and its movement between samples — covers. +These show how to choose the ROI and **Minimum Change Area** for two common goals. Minimum Change Area is the size of a single moving region as a percentage of the ROI you draw, so the right value depends on how much of the ROI your subject, and its movement between samples, covers. Because samples are a second or more apart, a moving subject usually appears in two places at once in the comparison, so even ordinary motion often scores tens of percent and a low threshold lets in almost everything. The most reliable approach is to **run a search, look at the percentage each result scored, and set Minimum Change Area just below the values for the events you care about.** The default is 20%; the suggestions below are starting points. -- **When did this item first appear (or disappear)?** A package was dropped off, a car parked, or a trash can was moved, and you want the exact moment. Draw a **tight ROI** around the spot the item occupies and **raise Minimum Change Area** (start around 40–60%). Because the item fills most of a tight ROI, its arrival or removal is a large change, while smaller nearby motion (shadows, a passing pedestrian) stays below the threshold. The **earliest result** is when it appeared; if you only care about that moment, a low Maximum Results finishes faster. If you get no hits, the ROI is probably looser than the item — lower the threshold or tighten the ROI. -- **What's been getting into the garden?** Something has been trampling a flower bed overnight and no object was ever tracked. Draw a **looser ROI** covering the whole bed and use a **lower Minimum Change Area than the case above** — start near the 20% default and lower it (toward 5–10%) only if a small or distant subject is missed, since it covers just a slice of a large region. Expect more results to scan through — step through the timestamps and jump to each to see what triggered it. If wind-blown plants add noise, raise Minimum Change Area or the Sensitivity Threshold. +- **When did this item first appear (or disappear)?** A package was dropped off, a car parked, or a trash can was moved, and you want the exact moment. Draw a **tight ROI** around the spot the item occupies and **raise Minimum Change Area** (start around 40–60%). Because the item fills most of a tight ROI, its arrival or removal is a large change, while smaller nearby motion (shadows, a passing pedestrian) stays below the threshold. The **earliest result** is when it appeared; if you only care about that moment, a low Maximum Results finishes faster. If you get no hits, the ROI is probably looser than the item: lower the threshold or tighten the ROI. +- **What's been getting into the garden?** Something has been trampling a flower bed overnight and no object was ever tracked. Draw a **looser ROI** covering the whole bed and use a **lower Minimum Change Area than the case above**: start near the 20% default and lower it (toward 5–10%) only if a small or distant subject is missed, since it covers just a slice of a large region. Expect more results to scan through: step through the timestamps and jump to each to see what triggered it. If wind-blown plants add noise, raise Minimum Change Area or the Sensitivity Threshold. #### Expected performance @@ -128,7 +128,7 @@ Motion Search analyzes the saved recordings on demand rather than reading a pre- To increase the speed of searches: -- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed — forcing you to lower Minimum Change Area, which lets in more noise. +- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed, forcing you to lower Minimum Change Area, which lets in more noise. - Narrow the time range to the window you care about, so there is less footage to examine. - Lower **Maximum Results** when you only need the first few hits. Because the search stops once it reaches that many results, a smaller value lets a busy range finish early instead of scanning the whole window. - Use Parallel mode to shorten wall-clock time on multi-core systems, at the cost of higher decoding and CPU usage while it runs. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index e11cdd5555..69ab7c3f70 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -186,6 +186,7 @@ const config: Config = { }, plugins: [ path.resolve(__dirname, "plugins", "raw-loader"), + path.resolve(__dirname, "plugins", "yaml-loader"), [ "docusaurus-plugin-openapi-docs", { diff --git a/docs/package.json b/docs/package.json index e57d7a1540..1649df7e0c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -29,6 +29,7 @@ "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", "js-yaml": "^4.1.1", + "marked": "^16.4.2", "prism-react-renderer": "^2.4.1", "raw-loader": "^4.0.2", "react": "^18.3.1", diff --git a/docs/plugins/js-yaml-loader.js b/docs/plugins/js-yaml-loader.js new file mode 100644 index 0000000000..66451bd3a9 --- /dev/null +++ b/docs/plugins/js-yaml-loader.js @@ -0,0 +1,9 @@ +const yaml = require("js-yaml"); + +// Webpack loader that compiles a YAML file into a default-exported JS object, +// so docs data can be authored in YAML (block scalars, no quote/newline +// escaping) but imported exactly like the JSON it replaces. +module.exports = function (source) { + const data = yaml.load(source); + return `export default ${JSON.stringify(data)};`; +}; diff --git a/docs/plugins/yaml-loader.js b/docs/plugins/yaml-loader.js new file mode 100644 index 0000000000..475940e6f2 --- /dev/null +++ b/docs/plugins/yaml-loader.js @@ -0,0 +1,23 @@ +const path = require("node:path"); + +// Enables importing YAML data files from docs/data as plain JS objects. +// Scoped to the data directory so it never intercepts other .yaml files +// (e.g. the OpenAPI spec under static/). +module.exports = function (context, options) { + return { + name: "yaml-data-loader", + configureWebpack(config, isServer, utils) { + return { + module: { + rules: [ + { + test: /\.ya?ml$/, + include: path.resolve(__dirname, "..", "data"), + use: path.resolve(__dirname, "js-yaml-loader.js"), + }, + ], + }, + }; + }, + }; +}; diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 14fb5ed413..ce6c41c133 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -132,7 +132,7 @@ const sidebars: SidebarsConfig = { { type: "link", label: "Go2RTC Configuration Reference", - href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration", + href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.14#configuration", } as PropSidebarItemLink, ], }, diff --git a/docs/src/components/FaqItem/index.jsx b/docs/src/components/FaqItem/index.jsx new file mode 100644 index 0000000000..af70d1100f --- /dev/null +++ b/docs/src/components/FaqItem/index.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from "react"; +import Heading from "@theme/Heading"; +import styles from "./styles.module.css"; + +// A single FAQ entry. +// +// The question is a real anchored heading (via @theme/Heading), so on desktop +// it gets the standard hover "#" hash link and the answer is always shown. On +// mobile the heading text is a button that toggles its answer, keeping long +// FAQ pages short. The desktop/mobile split is pure CSS (Docusaurus breakpoint: +// 996px), so there is no hydration flash. The answer is always rendered into +// the DOM, so search engines and the docs AI bot can read it regardless of +// layout or collapsed state. The heading id resolves deep links on both layouts +// and auto-expands the entry on mobile when it is the link target. +export default function FaqItem({ id, question, children }) { + const [open, setOpen] = useState(false); + + useEffect(() => { + const openIfTargeted = () => { + if (window.location.hash === `#${id}`) { + setOpen(true); + } + }; + openIfTargeted(); + window.addEventListener("hashchange", openIfTargeted); + return () => window.removeEventListener("hashchange", openIfTargeted); + }, [id]); + + const toggle = () => { + const next = !open; + setOpen(next); + // Reflect the entry in the URL like clicking the heading anchor, so an + // opened answer is shareable. Use replaceState to avoid history spam and + // an abrupt scroll. Clear it on close if it currently points here. + if (next) { + if (window.location.hash !== `#${id}`) { + window.history.replaceState(null, "", `#${id}`); + } + } else if (window.location.hash === `#${id}`) { + window.history.replaceState( + null, + "", + window.location.pathname + window.location.search, + ); + } + }; + + return ( +
+ + + +
+ {children} +
+
+ ); +} diff --git a/docs/src/components/FaqItem/styles.module.css b/docs/src/components/FaqItem/styles.module.css new file mode 100644 index 0000000000..bf348dc88a --- /dev/null +++ b/docs/src/components/FaqItem/styles.module.css @@ -0,0 +1,93 @@ +/* + * FAQ entry: collapsible on mobile, static heading + expanded answer on + * desktop. The split is pure CSS (Docusaurus breakpoint: 996px) so there is + * no hydration flash. The answer is always rendered into the DOM, so search + * engines and the docs AI bot can read it regardless of layout or state. + */ + +.item { + scroll-margin-top: calc(var(--ifm-navbar-height) + 1rem); +} + +.heading { + margin: 0; +} + +/* Mobile: the heading text is a full-width clickable toggle row. */ +.toggle { + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + padding: 0.85rem 0; + border: none; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + background: none; + color: inherit; + font: inherit; + text-align: left; + cursor: pointer; +} + +.toggle::before { + content: ""; + flex: 0 0 auto; + width: 0.5rem; + height: 0.5rem; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: rotate(-45deg); + transition: transform var(--ifm-transition-fast, 200ms) ease; +} + +.item[data-open] .toggle::before { + transform: rotate(45deg); +} + +.question { + flex: 1; + min-width: 0; +} + +.content { + display: none; + padding: 0 0 0.85rem; +} + +.item[data-open] .content { + display: block; +} + +/* Hide the hover hash link on mobile (no hover; avoids a stray empty line). */ +.heading :global(.hash-link) { + display: none; +} + +/* Desktop: render as a normal expanded heading + answer. */ +@media (min-width: 997px) { + .heading { + margin: 1.75rem 0 0.85rem; + } + + .toggle { + display: inline; + width: auto; + padding: 0; + border: none; + cursor: default; + } + + .toggle::before { + display: none; + } + + .content { + display: block; + padding: 0 0 0.5rem 1rem; + border-left: 2px solid var(--ifm-color-emphasis-200); + } + + .heading :global(.hash-link) { + display: inline; + } +} diff --git a/docs/src/components/ModelConfigDropdown/index.jsx b/docs/src/components/ModelConfigDropdown/index.jsx new file mode 100644 index 0000000000..737f47ec23 --- /dev/null +++ b/docs/src/components/ModelConfigDropdown/index.jsx @@ -0,0 +1,171 @@ +import React, { useState } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import ConfigTabs from "@site/src/components/ConfigTabs"; +import TabItem from "@theme/TabItem"; +import { marked } from "marked"; +import styles from "./styles.module.css"; + +marked.setOptions({ gfm: true }); + +/** + * @typedef {Object} Model + * @property {string} key + * @property {string} label + * @property {boolean} recommended + * @property {string} download Markdown for the "download the model" step. + * @property {string} ui Markdown for the Frigate UI configuration step. + * @property {string} yaml Raw YAML for the configuration step. + */ + +// Render a markdown string to React nodes. Fenced code blocks become Docusaurus +// CodeBlock components (so they get syntax highlighting and a copy button); +// everything else is marked-parsed to HTML. +function renderBlocks(md, keyPrefix) { + if (!md.trim()) return []; + const tokens = marked.lexer(md); + const nodes = []; + let buffer = []; + let idx = 0; + + const flush = () => { + if (buffer.length) { + buffer.links = tokens.links; + nodes.push( +
, + ); + buffer = []; + } + }; + + tokens.forEach((token) => { + if (token.type === "code") { + flush(); + const language = (token.lang || "text").split(/\s+/)[0]; + nodes.push( + + {token.text} + , + ); + } 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( +
+
{heading}
+ {renderBlocks(body, `adm${k}`)} +
, + ); + lastIndex = admonition.lastIndex; + k++; + } + nodes.push(...renderBlocks(md.slice(lastIndex), `seg${k}`)); + return nodes; +} + +function Markdown({ children }) { + return
{renderMarkdown(children)}
; +} + +function RecommendedBadge() { + return Recommended; +} + +/** + * @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 ( +
+
+
+

Step 1 — Choose a model

+
setIsOpen(!isOpen) : undefined} + > +
+ + {selectedModel.label} + {selectedModel.recommended && } + + {hasChoices && ( + {isOpen ? "▲" : "▼"} + )} +
+
+ + {isOpen && hasChoices && ( +
+ {models.map((model, index) => ( +
handleModelSelect(index)} + > + {model.label} + {model.recommended && } +
+ ))} +
+ )} +
+ +
+

Step 2 — Download the model

+ {selectedModel.download} +
+ +
+

Step 3 — Configure the detector

+ + + {selectedModel.ui} + + + {selectedModel.yaml} + + +
+
+
+ ); +} diff --git a/docs/src/components/ModelConfigDropdown/styles.module.css b/docs/src/components/ModelConfigDropdown/styles.module.css new file mode 100644 index 0000000000..06f0333344 --- /dev/null +++ b/docs/src/components/ModelConfigDropdown/styles.module.css @@ -0,0 +1,275 @@ +/* =================================================================== + ModelConfigDropdown — styles + =================================================================== */ + +.wrapper { + margin: 1.5rem 0; +} + +/* --- Dropdown button --- */ + +.dropdown { + display: inline-block; + width: 360px; + max-width: 100%; + text-align: left; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + background: var(--ifm-background-color); + cursor: pointer; + transition: + border-color 0.2s, + box-shadow 0.2s; +} + +[data-theme="light"] .dropdown { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .dropdown { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +.dropdown:hover { + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .dropdown:hover { + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +.dropdown.open { + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .dropdown.open { + border-color: var(--ifm-color-primary); +} + +/* Single-model detectors render the label without a clickable menu. */ +.dropdown.static { + cursor: default; +} + +.dropdown.static:hover { + border-color: var(--ifm-color-emphasis-400); + box-shadow: none; +} + +[data-theme="light"] .dropdown.static:hover { + border-color: #d0d7de; +} + +[data-theme="dark"] .dropdown.static:hover { + border-color: var(--ifm-color-emphasis-300); +} + +.dropdownContent { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + padding: 0.8rem 1rem; +} + +/* --- Model menu --- */ + +.menu { + margin-top: 0.25rem; + width: 360px; + max-width: 100%; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + overflow: hidden; + background: var(--ifm-background-color); +} + +[data-theme="light"] .menu { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .menu { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +.menuItem { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1rem; + cursor: pointer; + font-size: 0.95rem; + color: var(--ifm-font-color-base); + transition: background 0.15s; +} + +.menuItem:not(:last-child) { + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.menuItem:hover { + background: var(--ifm-color-emphasis-100); +} + +.menuItemActive { + font-weight: var(--ifm-font-weight-semibold); + background: var(--ifm-color-primary-lightest); +} + +[data-theme="dark"] .menuItem:hover { + background: #2b3139; +} + +[data-theme="dark"] .menuItemActive { + background: #2b3139; +} + +.modelName { + font-weight: var(--ifm-font-weight-semibold); + color: var(--ifm-font-color-base); + font-size: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.recommendedBadge { + display: inline-block; + background: var(--ifm-color-success); + color: #fff; + font-size: 0.7rem; + font-weight: 600; + padding: 2px 8px; + border-radius: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.arrow { + font-size: 0.7rem; + color: var(--ifm-font-color-secondary); + transition: transform 0.2s; +} + +.dropdown.open .arrow { + transform: rotate(180deg); +} + +/* --- Panel --- */ + +.panel { + margin-top: 0.5rem; + border: 1px solid var(--ifm-color-emphasis-400); + border-radius: 8px; + overflow: hidden; + background: var(--ifm-background-color); +} + +[data-theme="light"] .panel { + border: 1px solid #d0d7de; + background: #fff; +} + +[data-theme="dark"] .panel { + border: 1px solid var(--ifm-color-emphasis-300); + background: #21262d; +} + +/* --- Steps --- */ + +.step { + padding: 1rem; +} + +.step:not(:last-child) { + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.stepTitle { + margin: 0 0 0.75rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--ifm-font-color-base); +} + +/* Rendered markdown (download + Frigate UI instructions). */ + +.markdown { + font-size: 0.9rem; + line-height: 1.6; +} + +.markdown > :last-child { + margin-bottom: 0; +} + +.markdown a { + color: var(--ifm-color-primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.markdown table { + display: table; + width: 100%; + margin: 0.75rem 0; + font-size: 0.85rem; +} + +/* Docusaurus-style admonitions rendered from markdown. */ + +.admonition { + margin: 0.75rem 0; + padding: 0.75rem 1rem; + border-left: 4px solid var(--ifm-color-info); + border-radius: 4px; + background: var(--ifm-color-info-contrast-background); + font-size: 0.85rem; +} + +.admonition > :last-child { + margin-bottom: 0; +} + +.admonitionTitle { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 0.75rem; + margin-bottom: 0.4rem; + color: var(--ifm-color-info); +} + +.admonition_warning { + border-left-color: var(--ifm-color-warning); + background: var(--ifm-color-warning-contrast-background); +} + +.admonition_warning .admonitionTitle { + color: var(--ifm-color-warning-dark); +} + +.admonition_danger { + border-left-color: var(--ifm-color-danger); + background: var(--ifm-color-danger-contrast-background); +} + +.admonition_danger .admonitionTitle { + color: var(--ifm-color-danger-dark); +} + +.admonition_tip { + border-left-color: var(--ifm-color-success); + background: var(--ifm-color-success-contrast-background); +} + +.admonition_tip .admonitionTitle { + color: var(--ifm-color-success-dark); +} diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 146fce4e64..af3a019b4c 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -1,14 +1,14 @@ +# Generated by generate_api_auth_spec.py — do not edit by hand. +# Regenerate with: python3 generate_api_auth_spec.py +# The empty info.title is intentional: a docusaurus-openapi-docs convention +# that suppresses the generated API introduction page. openapi: 3.1.0 info: - # To avoid the introduction page we set the title to empty string - # https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/4e771d309f6defe395449b26cc3c65814d72cbcc/packages/docusaurus-plugin-openapi-docs/src/openapi/openapi.ts#L92-L129 - title: "" + title: '' version: 0.1.0 - servers: - url: https://demo.frigate.video/api - url: http://localhost:5001/api - paths: /auth/first_time_login: get: @@ -16,30 +16,33 @@ paths: - Auth summary: First Time Login description: |- + **Access:** Public — no authentication required. + Return whether the admin first-time login help flag is set in config. This endpoint is intentionally unauthenticated so the login page can query it before a user is authenticated. operationId: first_time_login_auth_first_time_login_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: [] + x-required-role: public /auth: get: tags: - Auth summary: Authenticate request - description: >- - Authenticates the current request based on proxy headers or JWT token. - This endpoint verifies authentication credentials and manages JWT token - refresh. On success, no JSON body is returned; authentication state is - communicated via response headers and cookies. + description: |- + **Access:** Public — no authentication required. + + Authenticates the current request based on proxy headers or JWT token. This endpoint verifies authentication credentials and manages JWT token refresh. On success, no JSON body is returned; authentication state is communicated via response headers and cookies. operationId: auth_auth_get responses: - "202": + '202': description: Authentication Accepted (no response body) content: application/json: @@ -50,65 +53,67 @@ paths: schema: type: string remote-role: - description: "Resolved role (e.g., admin, viewer, or custom)" + description: Resolved role (e.g., admin, viewer, or custom) schema: type: string Set-Cookie: description: May include refreshed JWT cookie when applicable schema: type: string - "401": + '401': description: Authentication Failed + security: [] + x-required-role: public /profile: get: tags: - Auth summary: Get user profile - description: >- - Returns the current authenticated user's profile including username, - role, and allowed cameras. This endpoint requires authentication and - returns information about the user's permissions. + description: |- + **Access:** Any authenticated user. + + Returns the current authenticated user's profile including username, role, and allowed cameras. This endpoint requires authentication and returns information about the user's permissions. operationId: profile_profile_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any /logout: get: tags: - Auth summary: Logout user - description: >- - Logs out the current user by clearing the session cookie. After logout, - subsequent requests will require re-authentication. + description: |- + **Access:** Public — no authentication required. + + Logs out the current user by clearing the session cookie. After logout, subsequent requests will require re-authentication. operationId: logout_logout_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: [] + x-required-role: public /login: post: tags: - Auth summary: Login with credentials - description: >- - Authenticates a user with username and password. Returns a JWT token as - a secure HTTP-only cookie that can be used for subsequent API requests. - The JWT token can also be retrieved from the response and used as a - Bearer token in the Authorization header. + description: |- + **Access:** Public — no authentication required. + Authenticates a user with username and password. Returns a JWT token as a secure HTTP-only cookie that can be used for subsequent API requests. The JWT token can also be retrieved from the response and used as a Bearer token in the Authorization header. Example using Bearer token: - ``` - - curl -H "Authorization: Bearer " - https://frigate_ip:8971/api/profile - + curl -H "Authorization: Bearer " https://frigate_ip:8971/api/profile ``` operationId: login_login_post requestBody: @@ -116,68 +121,79 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AppPostLoginBody" + $ref: '#/components/schemas/AppPostLoginBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: [] + x-required-role: public /users: get: tags: - Auth summary: Get all users - description: >- - Returns a list of all users with their usernames and roles. Requires - admin role. Each user object contains the username and assigned role. + description: |- + **Access:** Admin role required. + + Returns a list of all users with their usernames and roles. Requires admin role. Each user object contains the username and assigned role. operationId: get_users_users_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin post: tags: - Auth summary: Create new user - description: >- - Creates a new user with the specified username, password, and role. - Requires admin role. Password must be at least 12 characters long. + description: |- + **Access:** Admin role required. + + Creates a new user with the specified username, password, and role. Requires admin role. Password must be at least 12 characters long. operationId: create_user_users_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/AppPostUsersBody" + $ref: '#/components/schemas/AppPostUsersBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/users/{username}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /users/{username}: delete: tags: - Auth summary: Delete user - description: >- - Deletes a user by username. The built-in admin user cannot be deleted. - Requires admin role. Returns success message or error if user not found. + description: |- + **Access:** Admin role required. + + Deletes a user by username. The built-in admin user cannot be deleted. Requires admin role. Returns success message or error if user not found. operationId: delete_user_users__username__delete parameters: - name: username @@ -187,28 +203,29 @@ paths: type: string title: Username responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/users/{username}/password": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /users/{username}/password: put: tags: - Auth summary: Update user password - description: >- - Updates a user's password. Users can only change their own password - unless they have admin role. Requires the current password to verify - identity for non-admin users. Password must be at least 12 characters - long. If user changes their own password, a new JWT cookie is - automatically issued. + description: |- + **Access:** Any authenticated user. + + Updates a user's password. Users can only change their own password unless they have admin role. Requires the current password to verify identity for non-admin users. Password must be at least 12 characters long. If user changes their own password, a new JWT cookie is automatically issued. operationId: update_password_users__username__password_put parameters: - name: username @@ -222,28 +239,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AppPutPasswordBody" + $ref: '#/components/schemas/AppPutPasswordBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/users/{username}/role": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /users/{username}/role: put: tags: - Auth summary: Update user role - description: >- - Updates a user's role. The built-in admin user's role cannot be - modified. Requires admin role. Valid roles are defined in the - configuration. + description: |- + **Access:** Admin role required. + + Updates a user's role. The built-in admin user's role cannot be modified. Requires admin role. Valid roles are defined in the configuration. operationId: update_role_users__username__role_put parameters: - name: username @@ -257,19 +277,22 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AppPutRoleBody" + $ref: '#/components/schemas/AppPutRoleBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /go2rtc/streams: get: tags: @@ -277,44 +300,54 @@ paths: summary: Go2Rtc Streams operationId: go2rtc_streams_go2rtc_streams_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "/go2rtc/streams/{camera_name}": + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /go2rtc/streams/{stream_name}: get: tags: - Camera summary: Go2Rtc Camera Stream - operationId: go2rtc_camera_stream_go2rtc_streams__camera_name__get + operationId: go2rtc_camera_stream_go2rtc_streams__stream_name__get parameters: - - name: camera_name + - name: stream_name in: path required: true schema: anyOf: - type: string - - type: "null" - title: Camera Name + - type: 'null' + title: Stream Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/go2rtc/streams/{stream_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' put: tags: - Camera summary: Go2Rtc Add Stream - description: Add or update a go2rtc stream configuration. + description: |- + **Access:** Admin role required. + + Add or update a go2rtc stream configuration. operationId: go2rtc_add_stream_go2rtc_streams__stream_name__put parameters: - name: stream_name @@ -328,25 +361,31 @@ paths: required: false schema: type: string - default: "" + default: '' title: Src responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin delete: tags: - Camera summary: Go2Rtc Delete Stream - description: Delete a go2rtc stream. + description: |- + **Access:** Admin role required. + + Delete a go2rtc stream. operationId: go2rtc_delete_stream_go2rtc_streams__stream_name__delete parameters: - name: stream_name @@ -356,17 +395,20 @@ paths: type: string title: Stream Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /ffprobe: get: tags: @@ -379,7 +421,7 @@ paths: required: false schema: type: string - default: "" + default: '' title: Paths - name: detailed in: query @@ -389,23 +431,65 @@ paths: default: false title: Detailed responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' + /keyframe_analysis: + get: + tags: + - Camera + summary: Keyframe Analysis + description: |- + **Access:** Admin role required. + + Probe a camera's record stream and classify its keyframe spacing. + + Detects smart/+ codecs and long/variable GOPs that degrade recording. + operationId: keyframe_analysis_keyframe_analysis_get + parameters: + - name: camera + in: query + required: false + schema: + type: string + default: '' + title: Camera + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /ffprobe/snapshot: get: tags: - Camera summary: Ffprobe Snapshot - description: Get a snapshot from a stream URL using ffmpeg. + description: |- + **Access:** Admin role required. + + Get a snapshot from a stream URL using ffmpeg. operationId: ffprobe_snapshot_ffprobe_snapshot_get parameters: - name: url @@ -413,7 +497,7 @@ paths: required: false schema: type: string - default: "" + default: '' title: Url - name: timeout in: query @@ -423,30 +507,32 @@ paths: default: 10 title: Timeout responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /reolink/detect: get: tags: - Camera summary: Reolink Detect - description: >- + description: |- + **Access:** Admin role required. + Detect Reolink camera capabilities and recommend optimal protocol. - Queries the Reolink camera API to determine the camera's resolution - - and recommends either http-flv (for 5MP and below) or rtsp (for higher - resolutions). + and recommends either http-flv (for 5MP and below) or rtsp (for higher resolutions). operationId: reolink_detect_reolink_detect_get parameters: - name: host @@ -454,44 +540,46 @@ paths: required: false schema: type: string - default: "" + default: '' title: Host - name: username in: query required: false schema: type: string - default: "" + default: '' title: Username - name: password in: query required: false schema: type: string - default: "" + default: '' title: Password responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /onvif/probe: get: tags: - Camera summary: Probe ONVIF device - description: >- - Probe an ONVIF device to determine capabilities and optionally test - available stream URIs. Query params: host (required), port (default 80), - username, password, test (boolean), auth_type (basic or digest, default - basic). + description: |- + **Access:** Admin role required. + + Probe an ONVIF device to determine capabilities and optionally test available stream URIs. Query params: host (required), port (default 80), username, password, test (boolean), auth_type (basic or digest, default basic). operationId: onvif_probe_onvif_probe_get parameters: - name: host @@ -512,14 +600,14 @@ paths: required: false schema: type: string - default: "" + default: '' title: Username - name: password in: query required: false schema: type: string - default: "" + default: '' title: Password - name: test in: query @@ -536,23 +624,28 @@ paths: default: basic title: Auth Type responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/cameras/{camera_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cameras/{camera_name}: delete: tags: - Camera summary: Delete Camera description: |- + **Access:** Admin role required. + Delete a camera and all its associated data. Removes the camera from config, stops processes, and cleans up @@ -577,24 +670,31 @@ paths: default: false title: Delete Exports responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/camera/{camera_name}/set/{feature}/{sub_command}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /camera/{camera_name}/set/{feature}/{sub_command}: put: tags: - Camera summary: Camera Set - description: Set a camera feature state. Use camera_name='*' to target all cameras. - operationId: camera_set_camera__camera_name__set__feature___sub_command__put + description: |- + **Access:** Admin role required. + + Set a camera feature state. Use camera_name='*' to target all cameras. + operationId: + camera_set_camera__camera_name__set__feature___sub_command__put parameters: - name: camera_name in: path @@ -614,32 +714,38 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Sub Command requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/CameraSetBody" + $ref: '#/components/schemas/CameraSetBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/camera/{camera_name}/set/{feature}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /camera/{camera_name}/set/{feature}: put: tags: - Camera summary: Camera Set - description: Set a camera feature state. Use camera_name='*' to target all cameras. + description: |- + **Access:** Admin role required. + + Set a camera feature state. Use camera_name='*' to target all cameras. operationId: camera_set_camera__camera_name__set__feature__put parameters: - name: camera_name @@ -660,116 +766,208 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Sub Command requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/CameraSetBody" + $ref: '#/components/schemas/CameraSetBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /chat/tools: get: tags: - Chat summary: Get available tools - description: Returns OpenAI-compatible tool definitions for function calling. + description: |- + **Access:** Admin role required. + + Returns OpenAI-compatible tool definitions for function calling. operationId: get_tools_chat_tools_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /chat/execute: post: tags: - Chat summary: Execute a tool - description: Execute a tool function call from an LLM. + description: |- + **Access:** Admin role required. + + Execute a tool function call from an LLM. operationId: execute_tool_chat_execute_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ToolExecuteRequest" + $ref: '#/components/schemas/ToolExecuteRequest' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /chat/completion: post: tags: - Chat summary: Chat completion with tool calling - description: >- - Send a chat message to the configured GenAI provider with tool calling - support. The LLM can call Frigate tools to answer questions about your - cameras and events. + description: |- + **Access:** Admin role required. + + Send a chat message to the configured GenAI provider with tool calling support. The LLM can call Frigate tools to answer questions about your cameras and events. operationId: chat_completion_chat_completion_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ChatCompletionRequest" + $ref: '#/components/schemas/ChatCompletionRequest' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /vlm/monitor: + post: + tags: + - Chat + summary: Start a VLM watch job + description: |- + **Access:** Admin role required. + + Start monitoring a camera with the vision provider. The VLM analyzes live frames until the specified condition is met, then sends a notification. Only one watch job can run at a time. + operationId: start_vlm_monitor_vlm_monitor_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VLMMonitorRequest' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + get: + tags: + - Chat + summary: Get current VLM watch job + description: |- + **Access:** Admin role required. + + Returns the current (or most recently completed) VLM watch job. + operationId: get_vlm_monitor_vlm_monitor_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + delete: + tags: + - Chat + summary: Cancel the current VLM watch job + description: |- + **Access:** Admin role required. + + Cancels the running watch job if one exists. + operationId: cancel_vlm_monitor_vlm_monitor_delete + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /faces: get: tags: - Classification summary: Get all registered faces description: |- + **Access:** Admin role required. + Returns a dictionary mapping face names to lists of image filenames. Each key represents a registered face name, and the value is a list of image files associated with that face. Supported image formats include .webp, .png, .jpg, and .jpeg. operationId: get_faces_faces_get responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/FacesResponse" + $ref: '#/components/schemas/FacesResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin /faces/reprocess: post: tags: - Classification summary: Reprocess a face training image description: |- + **Access:** Admin role required. + Reprocesses a face training image to update the prediction. Requires face recognition to be enabled in the configuration. The training file must exist in the faces/train directory. Returns a success response or an error @@ -782,23 +980,28 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/faces/train/{name}/classify": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/train/{name}/classify: post: tags: - Classification summary: Classify and save a face training image description: |- + **Access:** Admin role required. + Adds a training image to a specific face name for face recognition. Accepts either a training file from the train directory or an event_id to extract the face from. The image is saved to the face's directory and the face classifier @@ -820,24 +1023,29 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/faces/{name}/create": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/create: post: tags: - Classification summary: Create a new face name description: |- + **Access:** Admin role required. + Creates a new folder for a face name in the faces directory. This is used to organize face training images. The face name is sanitized and spaces are replaced with underscores. Returns a success message or an error if @@ -851,26 +1059,30 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/faces/{name}/register": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/register: post: tags: - Classification summary: Register a face image - description: >- - Registers a face image for a specific face name by uploading an image - file. + description: |- + **Access:** Admin role required. + + Registers a face image for a specific face name by uploading an image file. The uploaded image is processed and added to the face recognition system. Returns a success response with details about the registration, or an error if face recognition is not enabled or the image cannot be processed. @@ -887,27 +1099,31 @@ paths: content: multipart/form-data: schema: - $ref: >- - #/components/schemas/Body_register_face_faces__name__register_post + $ref: '#/components/schemas/Body_register_face_faces__name__register_post' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /faces/recognize: post: tags: - Classification summary: Recognize a face from an uploaded image description: |- + **Access:** Admin role required. + Recognizes a face from an uploaded image file by comparing it against registered faces in the system. Returns the recognized face name and confidence score, or an error if face recognition is not enabled or the image cannot be processed. @@ -917,28 +1133,74 @@ paths: content: multipart/form-data: schema: - $ref: "#/components/schemas/Body_recognize_face_faces_recognize_post" + $ref: '#/components/schemas/Body_recognize_face_faces_recognize_post' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/FaceRecognitionResponse" - "422": + $ref: '#/components/schemas/FaceRecognitionResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/faces/{name}/delete": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/reclassify: + post: + tags: + - Classification + summary: Reclassify a face image to a different name + description: |- + **Access:** Admin role required. + + Moves a single face image from one person's folder to another. + The image is moved and renamed, and the face classifier is cleared to + incorporate the change. Returns a success message or an error if the + image or target name is invalid. + operationId: reclassify_face_image_faces__name__reclassify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{name}/delete: post: tags: - Classification summary: Delete face images - description: >- - Deletes specific face images for a given face name. The image IDs must - belong + description: |- + **Access:** Admin role required. + + Deletes specific face images for a given face name. The image IDs must belong to the specified face folder. To delete an entire face folder, all image IDs in that folder must be sent. Returns a success message or an error if face recognition is not enabled. operationId: deregister_faces_faces__name__delete_post @@ -954,26 +1216,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/DeleteFaceImagesBody" + $ref: '#/components/schemas/DeleteFaceImagesBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/faces/{old_name}/rename": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /faces/{old_name}/rename: put: tags: - Classification summary: Rename a face name description: |- + **Access:** Admin role required. + Renames a face name in the system. The old name must exist and the new name must be valid. Returns a success message or an error if face recognition is not enabled. operationId: rename_face_faces__old_name__rename_put @@ -989,26 +1256,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RenameFaceBody" + $ref: '#/components/schemas/RenameFaceBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /lpr/reprocess: put: tags: - Classification summary: Reprocess a license plate description: |- + **Access:** Admin role required. + Reprocesses a license plate image to update the plate. Requires license plate recognition to be enabled in the configuration. The event_id must exist in the database. Returns a success message or an error if license plate @@ -1022,39 +1294,49 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /reindex: put: tags: - Classification summary: Reindex embeddings description: |- + **Access:** Admin role required. + Reindexes the embeddings for all tracked objects. Requires semantic search to be enabled in the configuration. Returns a success message or an error if semantic search is not enabled. operationId: reindex_embeddings_reindex_put responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" + $ref: '#/components/schemas/GenericResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin /audio/transcribe: put: tags: - Classification summary: Transcribe audio description: |- + **Access:** Admin role required. + Transcribes audio from a specific event. Requires audio transcription to be enabled in the configuration. The event_id must exist in the database. Returns a success message or an error if audio transcription is not enabled or the event_id is invalid. @@ -1064,26 +1346,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AudioTranscriptionBody" + $ref: '#/components/schemas/AudioTranscriptionBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/dataset": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset: get: tags: - Classification summary: Get classification dataset description: |- + **Access:** Admin role required. + Gets the dataset for a specific classification model. The name must exist in the classification models. Returns a success message or an error if the name is invalid. operationId: get_classification_dataset_classification__name__dataset_get @@ -1095,23 +1382,28 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /classification/attributes: get: tags: - Classification summary: Get custom classification attributes description: |- + **Access:** Admin role required. + Returns custom classification attributes for a given object type. Only includes models with classification_type set to 'attribute'. By default returns a flat sorted list of all attribute labels. @@ -1132,23 +1424,28 @@ paths: default: false title: Group By Model responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/train": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/train: get: tags: - Classification summary: Get classification train images description: |- + **Access:** Admin role required. + Gets the train images for a specific classification model. The name must exist in the classification models. Returns a success message or an error if the name is invalid. operationId: get_classification_images_classification__name__train_get @@ -1160,22 +1457,27 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin post: tags: - Classification summary: Train a classification model description: |- + **Access:** Admin role required. + Trains a specific classification model. The name must exist in the classification models. Returns a success message or an error if the name is invalid. operationId: train_configured_model_classification__name__train_post @@ -1187,28 +1489,32 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/dataset/{category}/delete": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/delete: post: tags: - Classification summary: Delete classification dataset images - description: >- - Deletes specific dataset images for a given classification model and - category. + description: |- + **Access:** Admin role required. + + Deletes specific dataset images for a given classification model and category. The image IDs must belong to the specified category. Returns a success message or an error if the name or category is invalid. - operationId: >- + operationId: delete_classification_dataset_images_classification__name__dataset__category__delete_post parameters: - name: name @@ -1230,27 +1536,79 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/dataset/{old_category}/rename": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/reclassify: + post: + tags: + - Classification + summary: Reclassify a dataset image to a different category + description: |- + **Access:** Admin role required. + + Moves a single dataset image from one category to another. + The image is re-saved as PNG in the target category and removed from the source. + operationId: + reclassify_classification_image_classification__name__dataset__category__reclassify_post + parameters: + - name: name + in: path + required: true + schema: + type: string + title: Name + - name: category + in: path + required: true + schema: + type: string + title: Category + requestBody: + content: + application/json: + schema: + type: object + title: Body + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{old_category}/rename: put: tags: - Classification summary: Rename a classification category description: |- + **Access:** Admin role required. + Renames a classification category for a given classification model. The old category must exist and the new name must be valid. Returns a success message or an error if the name is invalid. - operationId: >- + operationId: rename_classification_category_classification__name__dataset__old_category__rename_put parameters: - name: name @@ -1272,28 +1630,32 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/dataset/categorize": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/categorize: post: tags: - Classification summary: Categorize a classification image - description: >- - Categorizes a specific classification image for a given classification - model and category. + description: |- + **Access:** Admin role required. + + Categorizes a specific classification image for a given classification model and category. The image must exist in the specified category. Returns a success message or an error if the name or category is invalid. - operationId: >- + operationId: categorize_classification_image_classification__name__dataset_categorize_post parameters: - name: name @@ -1309,28 +1671,33 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/dataset/{category}/create": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/dataset/{category}/create: post: tags: - Classification summary: Create an empty classification category folder description: |- + **Access:** Admin role required. + Creates an empty folder for a classification category. This is used to create folders for categories that don't have images yet. Returns a success message or an error if the name is invalid. - operationId: >- + operationId: create_classification_category_classification__name__dataset__category__create_post parameters: - name: name @@ -1346,27 +1713,32 @@ paths: type: string title: Category responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}/train/delete": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}/train/delete: post: tags: - Classification summary: Delete classification train images description: |- + **Access:** Admin role required. + Deletes specific train images for a given classification model. The image IDs must belong to the specified train folder. Returns a success message or an error if the name is invalid. - operationId: >- + operationId: delete_classification_train_images_classification__name__train_delete_post parameters: - name: name @@ -1382,76 +1754,95 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /classification/generate_examples/state: post: tags: - Classification summary: Generate state classification examples - description: Generate examples for state classification. - operationId: generate_state_examples_classification_generate_examples_state_post + description: |- + **Access:** Admin role required. + + Generate examples for state classification. + operationId: + generate_state_examples_classification_generate_examples_state_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/GenerateStateExamplesBody" + $ref: '#/components/schemas/GenerateStateExamplesBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /classification/generate_examples/object: post: tags: - Classification summary: Generate object classification examples - description: Generate examples for object classification. - operationId: generate_object_examples_classification_generate_examples_object_post + description: |- + **Access:** Admin role required. + + Generate examples for object classification. + operationId: + generate_object_examples_classification_generate_examples_object_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/GenerateObjectExamplesBody" + $ref: '#/components/schemas/GenerateObjectExamplesBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/classification/{name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /classification/{name}: delete: tags: - Classification summary: Delete a classification model description: |- + **Access:** Admin role required. + Deletes a specific classification model and all its associated data. Works even if the model is not in the config (e.g., partially created during wizard). Returns a success message. @@ -1464,18 +1855,21 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /review: get: tags: @@ -1520,7 +1914,7 @@ paths: in: query required: false schema: - $ref: "#/components/schemas/SeverityEnum" + $ref: '#/components/schemas/SeverityEnum' - name: before in: query required: false @@ -1534,21 +1928,25 @@ paths: type: number title: After responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/ReviewSegmentResponse" + $ref: '#/components/schemas/ReviewSegmentResponse' title: Response Review Review Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /review_ids: get: tags: @@ -1563,21 +1961,25 @@ paths: type: string title: Ids responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/ReviewSegmentResponse" + $ref: '#/components/schemas/ReviewSegmentResponse' title: Response Review Ids Review Ids Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' /review/summary: get: tags: @@ -1614,18 +2016,22 @@ paths: default: utc title: Timezone responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ReviewSummaryResponse" - "422": + $ref: '#/components/schemas/ReviewSummaryResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /reviews/viewed: post: tags: @@ -1637,20 +2043,24 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ReviewModifyMultipleBody" + $ref: '#/components/schemas/ReviewModifyMultipleBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' /reviews/delete: post: tags: @@ -1662,26 +2072,33 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ReviewModifyMultipleBody" + $ref: '#/components/schemas/ReviewModifyMultipleBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /review/activity/motion: get: tags: - Review summary: Motion Activity - description: Get motion and audio activity. + description: |- + **Access:** Any authenticated user. + + Get motion and audio activity. operationId: motion_activity_review_activity_motion_get parameters: - name: cameras @@ -1711,22 +2128,25 @@ paths: default: 30 title: Scale responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/ReviewActivityMotionResponse" + $ref: '#/components/schemas/ReviewActivityMotionResponse' title: Response Motion Activity Review Activity Motion Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/review/event/{event_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /review/event/{event_id}: get: tags: - Review @@ -1740,19 +2160,23 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ReviewSegmentResponse" - "422": + $ref: '#/components/schemas/ReviewSegmentResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/review/{review_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}: get: tags: - Review @@ -1766,19 +2190,23 @@ paths: type: string title: Review Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ReviewSegmentResponse" - "422": + $ref: '#/components/schemas/ReviewSegmentResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/review/{review_id}/viewed": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}/viewed: delete: tags: - Review @@ -1792,25 +2220,32 @@ paths: type: string title: Review Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/review/summarize/start/{start_ts}/end/{end_ts}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /review/summarize/start/{start_ts}/end/{end_ts}: post: tags: - Review summary: Generate Review Summary - description: Use GenAI to summarize review items over a period of time. - operationId: >- + description: |- + **Access:** Admin role required. + + Use GenAI to summarize review items over a period of time. + operationId: generate_review_summary_review_summarize_start__start_ts__end__end_ts__post parameters: - name: start_ts @@ -1826,17 +2261,20 @@ paths: type: number title: End Ts responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /: get: tags: @@ -1844,12 +2282,15 @@ paths: summary: Is Healthy operationId: is_healthy__get responses: - "200": + '200': description: Successful Response content: text/plain: schema: type: string + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' /config/schema.json: get: tags: @@ -1857,11 +2298,14 @@ paths: summary: Config Schema operationId: config_schema_config_schema_json_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' /version: get: tags: @@ -1869,12 +2313,15 @@ paths: summary: Version operationId: version_version_get responses: - "200": + '200': description: Successful Response content: text/plain: schema: type: string + security: [] + x-required-role: public + description: '**Access:** Public — no authentication required.' /stats: get: tags: @@ -1882,11 +2329,15 @@ paths: summary: Stats operationId: stats_stats_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /stats/history: get: tags: @@ -1901,30 +2352,90 @@ paths: type: string title: Keys responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /metrics: get: tags: - App summary: Metrics - description: Expose Prometheus metrics endpoint and update metrics with latest stats + description: |- + **Access:** Any authenticated user. + + Expose Prometheus metrics endpoint and update metrics with latest stats operationId: metrics_metrics_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + /genai/models: + get: + tags: + - App + summary: List available GenAI models + description: |- + **Access:** Admin role required. + + Returns available models for each configured GenAI provider. + operationId: genai_models_genai_models_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + /genai/probe: + post: + tags: + - App + summary: Probe a GenAI provider without saving config + description: |- + **Access:** Admin role required. + + Builds a transient client from the request body and returns its available models. Used to validate provider credentials in the UI before saving the configuration. + operationId: genai_probe_genai_probe_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenAIProbeBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /config: get: tags: @@ -1932,65 +2443,91 @@ paths: summary: Config operationId: config_config_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /profiles: get: tags: - App summary: Get Profiles - description: List all available profiles and the currently active profile. + description: |- + **Access:** Any authenticated user. + + List all available profiles and the currently active profile. operationId: get_profiles_profiles_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any /profile/active: get: tags: - App summary: Get Active Profile - description: Get the currently active profile. + description: |- + **Access:** Admin role required. + + Get the currently active profile. operationId: get_active_profile_profile_active_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /ffmpeg/presets: get: tags: - App summary: Ffmpeg Presets - description: Return available ffmpeg preset keys for config UI usage. + description: |- + **Access:** Admin role required. + + Return available ffmpeg preset keys for config UI usage. operationId: ffmpeg_presets_ffmpeg_presets_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /config/raw_paths: get: tags: - App summary: Config Raw Paths - description: >- - Admin-only endpoint that returns camera paths and go2rtc streams without - credential masking. + description: |- + **Access:** Admin role required. + + Admin-only endpoint that returns camera paths and go2rtc streams without credential masking. operationId: config_raw_paths_config_raw_paths_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin /config/raw: get: tags: @@ -1998,11 +2535,15 @@ paths: summary: Config Raw operationId: config_raw_config_raw_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /config/save: post: tags: @@ -2023,17 +2564,21 @@ paths: schema: title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /config/set: put: tags: @@ -2045,60 +2590,23 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AppConfigSetBody" + $ref: '#/components/schemas/AppConfigSetBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - /genai/models: - get: - tags: - - App - summary: List available GenAI models - description: Returns available models for each configured GenAI provider. - operationId: genai_models_genai_models_get - responses: - "200": - description: Successful Response - content: - application/json: - schema: {} - /genai/probe: - post: - tags: - - App - summary: Probe a GenAI provider without saving config - description: >- - Builds a transient client from the request body and returns its - available models. Used to validate provider credentials in the UI - before saving the configuration. Requires admin role. - operationId: genai_probe_genai_probe_post - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/GenAIProbeBody" - responses: - "200": - description: Successful Response - content: - application/json: - schema: {} - "422": - description: Validation Error - content: - application/json: - schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /vainfo: get: tags: @@ -2106,11 +2614,15 @@ paths: summary: Vainfo operationId: vainfo_vainfo_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /nvinfo: get: tags: @@ -2118,18 +2630,25 @@ paths: summary: Nvinfo operationId: nvinfo_nvinfo_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "/logs/{service}": + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /logs/{service}: get: tags: - App - Logs summary: Logs - description: Get logs for the requested service (frigate/nginx/go2rtc) + description: |- + **Access:** Admin role required. + + Get logs for the requested service (frigate/nginx/go2rtc) operationId: logs_logs__service__get parameters: - name: service @@ -2148,7 +2667,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Download - name: stream in: query @@ -2156,7 +2675,7 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' default: false title: Stream - name: start @@ -2165,7 +2684,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 0 title: Start - name: end @@ -2174,20 +2693,23 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: End responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /restart: post: tags: @@ -2195,19 +2717,24 @@ paths: summary: Restart operationId: restart_restart_post responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /media/sync: post: tags: - App summary: Start media sync job - description: >- - Start an asynchronous media sync job to find and (optionally) remove - orphaned media files. + description: |- + **Access:** Admin role required. + + Start an asynchronous media sync job to find and (optionally) remove orphaned media files. Returns 202 with job details when queued, or 409 if a job is already running. operationId: sync_media_media_sync_post requestBody: @@ -2215,43 +2742,51 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/MediaSyncBody" + $ref: '#/components/schemas/MediaSyncBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /media/sync/current: get: tags: - App summary: Get current media sync job - description: >- - Retrieve the current running media sync job, if any. Returns the job - details + description: |- + **Access:** Admin role required. + + Retrieve the current running media sync job, if any. Returns the job details or null when no job is active. operationId: get_media_sync_current_media_sync_current_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "/media/sync/status/{job_id}": + security: + - frigateAdminAuth: [] + x-required-role: admin + /media/sync/status/{job_id}: get: tags: - App summary: Get media sync job status - description: >- - Get status and results for the specified media sync job id. Returns 200 - with + description: |- + **Access:** Admin role required. + + Get status and results for the specified media sync job id. Returns 200 with job details including results, or 404 if the job is not found. operationId: get_media_sync_status_media_sync_status__job_id__get parameters: @@ -2262,17 +2797,20 @@ paths: type: string title: Job Id responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /labels: get: tags: @@ -2285,20 +2823,24 @@ paths: required: false schema: type: string - default: "" + default: '' title: Camera responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /sub_labels: get: tags: @@ -2312,20 +2854,24 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Split Joined responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /audio_labels: get: tags: @@ -2333,11 +2879,15 @@ paths: summary: Get Audio Labels operationId: get_audio_labels_audio_labels_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /plus/models: get: tags: @@ -2353,17 +2903,21 @@ paths: default: false title: Filterbycurrentmodeldetector responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /recognized_license_plates: get: tags: @@ -2377,20 +2931,24 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Split Joined responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /timeline: get: tags: @@ -2418,26 +2976,33 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Source Id responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' /timeline/hourly: get: tags: - App summary: Hourly Timeline - description: Get hourly summary for timeline. + description: |- + **Access:** Any authenticated user. + + Get hourly summary for timeline. operationId: hourly_timeline_timeline_hourly_get parameters: - name: cameras @@ -2446,7 +3011,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras - name: labels @@ -2455,7 +3020,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Labels - name: after @@ -2464,7 +3029,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: After - name: before in: query @@ -2472,7 +3037,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Before - name: limit in: query @@ -2480,7 +3045,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 200 title: Limit - name: timezone @@ -2489,32 +3054,38 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: utc title: Timezone responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/preview/{camera_name}/start/{start_ts}/end/{end_ts}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{camera_name}/start/{start_ts}/end/{end_ts}: get: tags: - Preview summary: Get preview clips for time range description: |- + **Access:** Any authenticated user. + Gets all preview clips for a specified camera and time range. Returns a list of preview video clips that overlap with the requested time period, ordered by start time. Use camera_name='all' to get previews from all cameras. Returns an error if no previews are found. - operationId: preview_ts_preview__camera_name__start__start_ts__end__end_ts__get + operationId: + preview_ts_preview__camera_name__start__start_ts__end__end_ts__get parameters: - name: camera_name in: path @@ -2535,34 +3106,38 @@ paths: type: number title: End Ts responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/PreviewModel" - title: >- - Response Preview Ts Preview Camera Name Start Start Ts + $ref: '#/components/schemas/PreviewModel' + title: Response Preview Ts Preview Camera Name Start Start Ts End End Ts Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: get: tags: - Preview summary: Get preview clips for specific hour description: |- + **Access:** Any authenticated user. + Gets all preview clips for a specific hour in a given timezone. Converts the provided date/time from the specified timezone to UTC and retrieves all preview clips for that hour. Use camera_name='all' to get previews from all cameras. The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes). - operationId: >- + operationId: preview_hour_preview__year_month___day___hour___camera_name___tz_name__get parameters: - name: year_month @@ -2596,34 +3171,37 @@ paths: type: string title: Tz Name responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/PreviewModel" - title: >- - Response Preview Hour Preview Year Month Day Hour + $ref: '#/components/schemas/PreviewModel' + title: Response Preview Hour Preview Year Month Day Hour Camera Name Tz Name Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames: get: tags: - Preview summary: Get cached preview frame filenames - description: >- - Gets a list of cached preview frame filenames for a specific camera and - time range. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets a list of cached preview frame filenames for a specific camera and time range. Returns an array of filenames for preview frames that fall within the specified time period, sorted in chronological order. These are individual frame images cached for quick preview display. - operationId: >- + operationId: get_preview_frames_from_cache_preview__camera_name__start__start_ts__end__end_ts__frames_get parameters: - name: camera_name @@ -2632,7 +3210,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -2647,7 +3225,7 @@ paths: type: number title: End Ts responses: - "200": + '200': description: Successful Response content: application/json: @@ -2655,36 +3233,45 @@ paths: type: array items: type: string - title: >- - Response Get Preview Frames From Cache Preview Camera Name - Start Start Ts End End Ts Frames Get - "422": + title: Response Get Preview Frames From Cache Preview Camera + Name Start Start Ts End End Ts Frames Get + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera /notifications/pubkey: get: tags: - Notifications summary: Get VAPID public key description: |- + **Access:** Any authenticated user. + Gets the VAPID public key for the notifications. Returns the public key or an error if notifications are not enabled. operationId: get_vapid_pub_key_notifications_pubkey_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateUserAuth: [] + x-required-role: any /notifications/register: post: tags: - Notifications summary: Register notifications description: |- + **Access:** Any authenticated user. + Registers a notifications subscription. Returns a success message or an error if the subscription is not provided. operationId: register_notifications_notifications_register_post @@ -2695,23 +3282,28 @@ paths: type: object title: Body responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any /exports: get: tags: - Export summary: Get exports description: |- + **Access:** Any authenticated user. + Gets all exports from the database for cameras the user has access to. Returns a list of exports ordered by date (most recent first). operationId: get_exports_exports_get @@ -2722,7 +3314,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Export Case Id - name: cameras in: query @@ -2730,7 +3322,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras - name: start_date @@ -2739,7 +3331,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Start Date - name: end_date in: query @@ -2747,201 +3339,90 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: End Date responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/ExportModel" + $ref: '#/components/schemas/ExportModel' title: Response Get Exports Exports Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - /exports/batch: - post: - tags: - - Export - summary: Start recording export batch - description: >- - Starts recording exports for a batch of items, each with its own camera - and time range. Optionally assigns them to a new or existing export case. - When neither export_case_id nor new_case_name is provided, exports are - added as uncategorized. Attaching to an existing case is admin-only. - operationId: export_recordings_batch_exports_batch_post - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/BatchExportBody" - responses: - "202": - description: Successful Response - content: - application/json: - schema: - $ref: "#/components/schemas/BatchExportResponse" - "400": - description: Bad Request - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "403": - description: Forbidden - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "404": - description: Not Found - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "503": - description: Service Unavailable - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "422": - description: Validation Error - content: - application/json: - schema: - $ref: "#/components/schemas/HTTPValidationError" - /exports/delete: - post: - tags: - - Export - summary: Bulk delete exports - description: >- - Deletes one or more exports by ID. All IDs must exist and none can be - in-progress. Admin-only. - operationId: bulk_delete_exports_exports_delete_post - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/ExportBulkDeleteBody" - responses: - "200": - description: Successful Response - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "400": - description: Bad Request - one or more exports are in-progress - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "404": - description: Not Found - one or more export IDs do not exist - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "422": - description: Validation Error - content: - application/json: - schema: - $ref: "#/components/schemas/HTTPValidationError" - /exports/reassign: - post: - tags: - - Export - summary: Bulk reassign exports to a case - description: >- - Assigns or unassigns one or more exports to/from a case. All IDs must - exist. Pass export_case_id as null to unassign (move to uncategorized). - Admin-only. - operationId: bulk_reassign_exports_exports_reassign_post - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/ExportBulkReassignBody" - responses: - "200": - description: Successful Response - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "404": - description: Not Found - one or more export IDs or the target case do not exist - content: - application/json: - schema: - $ref: "#/components/schemas/GenericResponse" - "422": - description: Validation Error - content: - application/json: - schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any /cases: get: tags: - Export summary: Get export cases - description: Gets all export cases from the database. + description: |- + **Access:** Any authenticated user. + + Gets all export cases from the database. operationId: get_export_cases_cases_get responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/ExportCaseModel" + $ref: '#/components/schemas/ExportCaseModel' title: Response Get Export Cases Cases Get + security: + - frigateUserAuth: [] + x-required-role: any post: tags: - Export summary: Create export case - description: Creates a new export case. + description: |- + **Access:** Admin role required. + + Creates a new export case. operationId: create_export_case_cases_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ExportCaseCreateBody" + $ref: '#/components/schemas/ExportCaseCreateBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ExportCaseModel" - "422": + $ref: '#/components/schemas/ExportCaseModel' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/cases/{case_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cases/{case_id}: get: tags: - Export summary: Get a single export case - description: Gets a specific export case by ID. + description: |- + **Access:** Any authenticated user. + + Gets a specific export case by ID. operationId: get_export_case_cases__case_id__get parameters: - name: case_id @@ -2951,23 +3432,29 @@ paths: type: string title: Case Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ExportCaseModel" - "422": + $ref: '#/components/schemas/ExportCaseModel' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any patch: tags: - Export summary: Update export case - description: Updates an existing export case. + description: |- + **Access:** Admin role required. + + Updates an existing export case. operationId: update_export_case_cases__case_id__patch parameters: - name: case_id @@ -2981,25 +3468,30 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ExportCaseUpdateBody" + $ref: '#/components/schemas/ExportCaseUpdateBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin delete: tags: - Export summary: Delete export case description: |- + **Access:** Admin role required. + Deletes an export case. Exports that reference this case will have their export_case set to null. operationId: delete_export_case_cases__case_id__delete @@ -3010,30 +3502,162 @@ paths: schema: type: string title: Case Id + - name: delete_exports + in: query + required: false + schema: + type: boolean + default: false + title: Delete Exports responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/export/{camera_name}/start/{start_time}/end/{end_time}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /cases/{case_id}/download: + get: + tags: + - Export + summary: Download export case as zip + description: |- + **Access:** Any authenticated user. + + Streams a zip archive containing every completed export's mp4 for the given case. + operationId: download_export_case_cases__case_id__download_get + parameters: + - name: case_id + in: path + required: true + schema: + type: string + title: Case Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /jobs/export: + get: + tags: + - Export + summary: Get active export jobs + description: |- + **Access:** Any authenticated user. + + Gets queued and running export jobs. + operationId: get_active_export_jobs_jobs_export_get + responses: + '200': + description: Successful Response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ExportJobModel' + title: Response Get Active Export Jobs Jobs Export Get + security: + - frigateUserAuth: [] + x-required-role: any + /jobs/export/{export_id}: + get: + tags: + - Export + summary: Get export job status + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets queued, running, or completed status for a specific export job. + operationId: get_export_job_status_jobs_export__export_id__get + parameters: + - name: export_id + in: path + required: true + schema: + type: string + title: Export Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/ExportJobModel' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/batch: + post: + tags: + - Export + summary: Start recording export batch + description: |- + **Access:** Any authenticated user. + + Starts recording exports for a batch of items, each with its own camera and time range, and assigns them to a single export case. Attaching to an existing case is temporarily admin-only until case-level ACLs exist. + operationId: export_recordings_batch_exports_batch_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BatchExportBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/BatchExportResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /export/{camera_name}/start/{start_time}/end/{end_time}: post: tags: - Export summary: Start recording export description: |- + **Access:** Authenticated user with access to the referenced camera. + Starts an export of a recording for the specified time range. The export can be from recordings or preview footage. Returns the export ID if successful, or an error message if the camera is invalid or no recordings/previews are found for the time range. - operationId: >- + operationId: export_recording_export__camera_name__start__start_time__end__end_time__post parameters: - name: camera_name @@ -3042,7 +3666,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_time in: path @@ -3061,26 +3685,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ExportRecordingsBody" + $ref: '#/components/schemas/ExportRecordingsBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/StartExportResponse" - "422": + $ref: '#/components/schemas/StartExportResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/export/{event_id}/rename": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /export/{event_id}/rename: patch: tags: - Export summary: Rename export description: |- + **Access:** Admin role required. + Renames an export. NOTE: This changes the friendly name of the export, not the filename. operationId: export_rename_export__event_id__rename_patch @@ -3096,33 +3725,37 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ExportRenameBody" + $ref: '#/components/schemas/ExportRenameBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/export/custom/{camera_name}/start/{start_time}/end/{end_time}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /export/custom/{camera_name}/start/{start_time}/end/{end_time}: post: tags: - Export summary: Start custom recording export - description: >- - Starts an export of a recording for the specified time range using - custom FFmpeg arguments. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Starts an export of a recording for the specified time range using custom FFmpeg arguments. The export can be from recordings or preview footage. Returns the export ID if successful, or an error message if the camera is invalid or no recordings/previews are found for the time range. If ffmpeg_input_args and ffmpeg_output_args are not provided, defaults to timelapse export settings. - operationId: >- + operationId: export_recording_custom_export_custom__camera_name__start__start_time__end__end_time__post parameters: - name: camera_name @@ -3131,7 +3764,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_time in: path @@ -3150,26 +3783,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ExportRecordingsCustomBody" + $ref: '#/components/schemas/ExportRecordingsCustomBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/StartExportResponse" - "422": + $ref: '#/components/schemas/StartExportResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/exports/{export_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/{export_id}: get: tags: - Export summary: Get a single export description: |- + **Access:** Authenticated user with access to the referenced camera. + Gets a specific export by ID. The user must have access to the camera associated with the export. operationId: get_export_exports__export_id__get @@ -3181,24 +3819,94 @@ paths: type: string title: Export Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/ExportModel" - "422": + $ref: '#/components/schemas/ExportModel' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /exports/delete: + post: + tags: + - Export + summary: Bulk delete exports + description: |- + **Access:** Admin role required. + + Deletes one or more exports by ID. All IDs must exist and none can be in-progress. + operationId: bulk_delete_exports_exports_delete_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportBulkDeleteBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /exports/reassign: + post: + tags: + - Export + summary: Bulk reassign exports to a case + description: |- + **Access:** Admin role required. + + Assigns or unassigns one or more exports to/from a case. All IDs must exist. + operationId: bulk_reassign_exports_exports_reassign_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportBulkReassignBody' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/GenericResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /events: get: tags: - Events summary: Get events - description: Returns a list of events. + description: |- + **Access:** Any authenticated user. + + Returns a list of events. operationId: events_events_get parameters: - name: camera @@ -3207,7 +3915,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Camera - name: cameras @@ -3216,7 +3924,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras - name: label @@ -3225,7 +3933,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Label - name: labels @@ -3234,7 +3942,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Labels - name: sub_label @@ -3243,7 +3951,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Sub Label - name: sub_labels @@ -3252,7 +3960,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Sub Labels - name: attributes @@ -3261,7 +3969,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Attributes - name: zone @@ -3270,7 +3978,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Zone - name: zones @@ -3279,7 +3987,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Zones - name: limit @@ -3288,7 +3996,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 100 title: Limit - name: after @@ -3297,7 +4005,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: After - name: before in: query @@ -3305,7 +4013,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Before - name: time_range in: query @@ -3313,8 +4021,8 @@ paths: schema: anyOf: - type: string - - type: "null" - default: "00:00,24:00" + - type: 'null' + default: 00:00,24:00 title: Time Range - name: has_clip in: query @@ -3322,7 +4030,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Has Clip - name: has_snapshot in: query @@ -3330,7 +4038,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Has Snapshot - name: in_progress in: query @@ -3338,7 +4046,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: In Progress - name: include_thumbnails in: query @@ -3346,7 +4054,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 1 title: Include Thumbnails - name: favorites @@ -3355,7 +4063,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Favorites - name: min_score in: query @@ -3363,7 +4071,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Min Score - name: max_score in: query @@ -3371,7 +4079,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Max Score - name: min_speed in: query @@ -3379,7 +4087,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Min Speed - name: max_speed in: query @@ -3387,7 +4095,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Max Speed - name: recognized_license_plate in: query @@ -3395,7 +4103,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Recognized License Plate - name: is_submitted @@ -3404,7 +4112,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Is Submitted - name: min_length in: query @@ -3412,7 +4120,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Min Length - name: max_length in: query @@ -3420,7 +4128,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Max Length - name: event_id in: query @@ -3428,7 +4136,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Event Id - name: sort in: query @@ -3436,7 +4144,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Sort - name: timezone in: query @@ -3444,31 +4152,36 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: utc title: Timezone responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/EventResponse" + $ref: '#/components/schemas/EventResponse' title: Response Events Events Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any /events/explore: get: tags: - Events summary: Get summary of objects description: |- + **Access:** Any authenticated user. + Gets a summary of objects from the database. Returns a list of objects with a max of `limit` objects for each label. operationId: events_explore_events_explore_get @@ -3481,27 +4194,32 @@ paths: default: 10 title: Limit responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/EventResponse" + $ref: '#/components/schemas/EventResponse' title: Response Events Explore Events Explore Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any /event_ids: get: tags: - Events summary: Get events by ids description: |- + **Access:** Authenticated user with access to the referenced camera. + Gets events by a list of ids. Returns a list of events. operationId: event_ids_event_ids_get @@ -3513,27 +4231,32 @@ paths: type: string title: Ids responses: - "200": + '200': description: Successful Response content: application/json: schema: type: array items: - $ref: "#/components/schemas/EventResponse" + $ref: '#/components/schemas/EventResponse' title: Response Event Ids Event Ids Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera /events/search: get: tags: - Events summary: Search events description: |- + **Access:** Any authenticated user. + Searches for events in the database. Returns a list of events. operationId: events_search_events_search_get @@ -3544,7 +4267,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Query - name: event_id in: query @@ -3552,7 +4275,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Event Id - name: search_type in: query @@ -3560,7 +4283,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: thumbnail title: Search Type - name: include_thumbnails @@ -3569,7 +4292,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 1 title: Include Thumbnails - name: limit @@ -3578,7 +4301,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 50 title: Limit - name: cameras @@ -3587,7 +4310,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras - name: labels @@ -3596,7 +4319,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Labels - name: sub_labels @@ -3605,7 +4328,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Sub Labels - name: attributes @@ -3614,7 +4337,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Attributes - name: zones @@ -3623,7 +4346,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Zones - name: after @@ -3632,7 +4355,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: After - name: before in: query @@ -3640,7 +4363,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Before - name: time_range in: query @@ -3648,8 +4371,8 @@ paths: schema: anyOf: - type: string - - type: "null" - default: "00:00,24:00" + - type: 'null' + default: 00:00,24:00 title: Time Range - name: has_clip in: query @@ -3657,7 +4380,7 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' title: Has Clip - name: has_snapshot in: query @@ -3665,7 +4388,7 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' title: Has Snapshot - name: is_submitted in: query @@ -3673,7 +4396,7 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' title: Is Submitted - name: timezone in: query @@ -3681,7 +4404,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: utc title: Timezone - name: min_score @@ -3690,7 +4413,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Min Score - name: max_score in: query @@ -3698,7 +4421,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Max Score - name: min_speed in: query @@ -3706,7 +4429,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Min Speed - name: max_speed in: query @@ -3714,7 +4437,7 @@ paths: schema: anyOf: - type: number - - type: "null" + - type: 'null' title: Max Speed - name: recognized_license_plate in: query @@ -3722,7 +4445,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Recognized License Plate - name: sort @@ -3731,20 +4454,23 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Sort responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any /events/summary: get: tags: @@ -3758,7 +4484,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: utc title: Timezone - name: has_clip @@ -3767,7 +4493,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Has Clip - name: has_snapshot in: query @@ -3775,26 +4501,33 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Has Snapshot responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + description: '**Access:** Any authenticated user.' + /events/{event_id}: get: tags: - Events summary: Get event by id - description: Gets an event by its id. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Gets an event by its id. operationId: event_events__event_id__get parameters: - name: event_id @@ -3804,23 +4537,28 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/EventResponse" - "422": + $ref: '#/components/schemas/EventResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera delete: tags: - Events summary: Delete event description: |- + **Access:** Admin role required. + Deletes an event from the database. Returns a success message or an error if the event is not found. operationId: delete_event_events__event_id__delete @@ -3832,24 +4570,29 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/retain": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/retain: post: tags: - Events summary: Set event retain indefinitely. description: |- + **Access:** Admin role required. + Sets an event to retain indefinitely. Returns a success message or an error if the event is not found. NOTE: This is a legacy endpoint and is not supported in the frontend. @@ -3862,23 +4605,28 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin delete: tags: - Events summary: Stop event from being retained indefinitely description: |- + **Access:** Admin role required. + Stops an event from being retained indefinitely. Returns a success message or an error if the event is not found. NOTE: This is a legacy endpoint and is not supported in the frontend. @@ -3891,24 +4639,29 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/plus": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/plus: post: tags: - Events summary: Send event to Frigate+ description: |- + **Access:** Admin role required. + Sends an event to Frigate+. Returns a success message or an error if the event is not found. operationId: send_to_plus_events__event_id__plus_post @@ -3923,26 +4676,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SubmitPlusBody" + $ref: '#/components/schemas/SubmitPlusBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/EventUploadPlusResponse" - "422": + $ref: '#/components/schemas/EventUploadPlusResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/false_positive": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/false_positive: put: tags: - Events summary: Submit false positive to Frigate+ description: |- + **Access:** Admin role required. + Submit an event as a false positive to Frigate+. This endpoint is the same as the standard Frigate+ submission endpoint, but is specifically for marking an event as a false positive. @@ -3955,24 +4713,29 @@ paths: type: string title: Event Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/EventUploadPlusResponse" - "422": + $ref: '#/components/schemas/EventUploadPlusResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/sub_label": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/sub_label: post: tags: - Events summary: Set event sub label description: |- + **Access:** Admin role required. + Sets an event's sub label. Returns a success message or an error if the event is not found. operationId: set_sub_label_events__event_id__sub_label_post @@ -3988,26 +4751,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsSubLabelBody" + $ref: '#/components/schemas/EventsSubLabelBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/recognized_license_plate": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/recognized_license_plate: post: tags: - Events summary: Set event license plate description: |- + **Access:** Admin role required. + Sets an event's license plate. Returns a success message or an error if the event is not found. operationId: set_plate_events__event_id__recognized_license_plate_post @@ -4023,28 +4791,32 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsLPRBody" + $ref: '#/components/schemas/EventsLPRBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/attributes": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/attributes: post: tags: - Events summary: Set custom classification attributes - description: >- - Sets an event's custom classification attributes for all attribute-type - models that apply to the event's object type. + description: |- + **Access:** Admin role required. + + Sets an event's custom classification attributes for all attribute-type models that apply to the event's object type. operationId: set_attributes_events__event_id__attributes_post parameters: - name: event_id @@ -4058,26 +4830,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsAttributesBody" + $ref: '#/components/schemas/EventsAttributesBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/description": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/description: post: tags: - Events summary: Set event description description: |- + **Access:** Admin role required. + Sets an event's description. Returns a success message or an error if the event is not found. operationId: set_description_events__event_id__description_post @@ -4093,29 +4870,35 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsDescriptionBody" + $ref: '#/components/schemas/EventsDescriptionBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/description/regenerate": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/description/regenerate: put: tags: - Events summary: Regenerate event description description: |- + **Access:** Admin role required. + Regenerates an event's description. Returns a success message or an error if the event is not found. - operationId: regenerate_description_events__event_id__description_regenerate_put + operationId: + regenerate_description_events__event_id__description_regenerate_put parameters: - name: event_id in: path @@ -4128,8 +4911,8 @@ paths: required: false schema: anyOf: - - $ref: "#/components/schemas/RegenerateDescriptionEnum" - - type: "null" + - $ref: '#/components/schemas/RegenerateDescriptionEnum' + - type: 'null' default: thumbnails title: Source - name: force @@ -4138,28 +4921,33 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' default: false title: Force responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /description/generate: post: tags: - Events summary: Generate description embedding description: |- + **Access:** Admin role required. + Generates an embedding for an event's description. Returns a success message or an error if the event is not found. operationId: generate_description_embedding_description_generate_post @@ -4168,26 +4956,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsDescriptionBody" + $ref: '#/components/schemas/EventsDescriptionBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /events/: delete: tags: - Events summary: Delete events description: |- + **Access:** Admin role required. + Deletes a list of events from the database. Returns a success message or an error if the events are not found. operationId: delete_events_events__delete @@ -4196,26 +4989,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsDeleteBody" + $ref: '#/components/schemas/EventsDeleteBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/EventMultiDeleteResponse" - "422": + $ref: '#/components/schemas/EventMultiDeleteResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{camera_name}/{label}/create": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{camera_name}/{label}/create: post: tags: - Events summary: Create manual event description: |- + **Access:** Admin role required. + Creates a manual event in the database. Returns a success message or an error if the event is not found. NOTES: @@ -4239,31 +5037,36 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsCreateBody" + $ref: '#/components/schemas/EventsCreateBody' default: - score: 0 + score: 0.0 duration: 30 include_recording: true draw: {} responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/EventCreateResponse" - "422": + $ref: '#/components/schemas/EventCreateResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/end": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/end: put: tags: - Events summary: End manual event description: |- + **Access:** Admin role required. + Ends a manual event. Returns a success message or an error if the event is not found. NOTE: This should only be used for manual events. @@ -4280,26 +5083,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/EventsEndBody" + $ref: '#/components/schemas/EventsEndBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /trigger/embedding: post: tags: - Events summary: Create trigger embedding description: |- + **Access:** Admin role required. + Creates a trigger embedding for a specific trigger. Returns a success message or an error if the trigger is not found. operationId: create_trigger_embedding_trigger_embedding_post @@ -4321,30 +5129,36 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TriggerEmbeddingBody" + $ref: '#/components/schemas/TriggerEmbeddingBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: type: object title: Response Create Trigger Embedding Trigger Embedding Post - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/trigger/embedding/{camera_name}/{name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /trigger/embedding/{camera_name}/{name}: put: tags: - Events summary: Update trigger embedding description: |- + **Access:** Admin role required. + Updates a trigger embedding for a specific trigger. Returns a success message or an error if the trigger is not found. - operationId: update_trigger_embedding_trigger_embedding__camera_name___name__put + operationId: + update_trigger_embedding_trigger_embedding__camera_name___name__put parameters: - name: camera_name in: path @@ -4363,31 +5177,36 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TriggerEmbeddingBody" + $ref: '#/components/schemas/TriggerEmbeddingBody' responses: - "200": + '200': description: Successful Response content: application/json: schema: type: object - title: >- - Response Update Trigger Embedding Trigger Embedding Camera - Name Name Put - "422": + title: Response Update Trigger Embedding Trigger Embedding + Camera Name Name Put + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin delete: tags: - Events summary: Delete trigger embedding description: |- + **Access:** Admin role required. + Deletes a trigger embedding for a specific trigger. Returns a success message or an error if the trigger is not found. - operationId: delete_trigger_embedding_trigger_embedding__camera_name___name__delete + operationId: + delete_trigger_embedding_trigger_embedding__camera_name___name__delete parameters: - name: camera_name in: path @@ -4402,27 +5221,31 @@ paths: type: string title: Name responses: - "200": + '200': description: Successful Response content: application/json: schema: type: object - title: >- - Response Delete Trigger Embedding Trigger Embedding Camera - Name Name Delete - "422": + title: Response Delete Trigger Embedding Trigger Embedding + Camera Name Name Delete + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/triggers/status/{camera_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /triggers/status/{camera_name}: get: tags: - Events summary: Get triggers status description: |- + **Access:** Admin role required. + Gets the status of all triggers for a specific camera. Returns a success message or an error if the camera is not found. operationId: get_triggers_status_triggers_status__camera_name__get @@ -4434,20 +5257,24 @@ paths: type: string title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: type: object - title: Response Get Triggers Status Triggers Status Camera Name Get - "422": + title: Response Get Triggers Status Triggers Status Camera Name + Get + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /{camera_name}: get: tags: - Media @@ -4460,7 +5287,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: fps in: query @@ -4482,7 +5309,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Bbox - name: timestamp in: query @@ -4490,7 +5317,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Timestamp - name: zones in: query @@ -4498,7 +5325,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Zones - name: mask in: query @@ -4506,7 +5333,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Mask - name: motion in: query @@ -4514,7 +5341,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Motion - name: regions in: query @@ -4522,21 +5349,25 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Regions responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/ptz/info": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/ptz/info: get: tags: - Media @@ -4549,29 +5380,33 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/latest.{extension}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/latest.{extension}: get: tags: - Media summary: Latest Frame - description: >- - Returns the latest frame from the specified camera in the requested - format (jpg, png, webp). Falls back to preview frames if the camera is - offline. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the latest frame from the specified camera in the requested format (jpg, png, webp). Falls back to preview frames if the camera is offline. operationId: latest_frame__camera_name__latest__extension__get parameters: - name: camera_name @@ -4580,20 +5415,20 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: extension in: path required: true schema: - $ref: "#/components/schemas/Extension" + $ref: '#/components/schemas/Extension' - name: bbox in: query required: false schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Bbox - name: timestamp in: query @@ -4601,7 +5436,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Timestamp - name: zones in: query @@ -4609,7 +5444,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Zones - name: mask in: query @@ -4617,7 +5452,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Mask - name: motion in: query @@ -4625,7 +5460,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Motion - name: paths in: query @@ -4633,7 +5468,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Paths - name: regions in: query @@ -4641,7 +5476,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Regions - name: quality in: query @@ -4649,7 +5484,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' default: 70 title: Quality - name: height @@ -4658,7 +5493,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Height - name: store in: query @@ -4666,26 +5501,29 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Store responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/recordings/{frame_time}/snapshot.{format}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/recordings/{frame_time}/snapshot.{format}: get: tags: - Media summary: Get Snapshot From Recording - operationId: >- + operationId: get_snapshot_from_recording__camera_name__recordings__frame_time__snapshot__format__get parameters: - name: camera_name @@ -4694,7 +5532,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: frame_time in: path @@ -4718,23 +5556,28 @@ paths: type: integer title: Height responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/plus/{frame_time}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/plus/{frame_time}: post: tags: - Media summary: Submit Recording Snapshot To Plus - operationId: submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post + operationId: + submit_recording_snapshot_to_plus__camera_name__plus__frame_time__post parameters: - name: camera_name in: path @@ -4742,7 +5585,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: frame_time in: path @@ -4751,26 +5594,32 @@ paths: type: string title: Frame Time responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/clip.mp4: get: tags: - Media summary: Recording Clip - description: >- - For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. - Safari does not reliably process progressive mp4 files. - operationId: recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get + description: |- + **Access:** Authenticated user with access to the referenced camera. + + For iOS devices, use the master.m3u8 HLS link instead of clip.mp4. Safari does not reliably process progressive mp4 files. + operationId: + recording_clip__camera_name__start__start_ts__end__end_ts__clip_mp4_get parameters: - name: camera_name in: path @@ -4778,7 +5627,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -4793,25 +5642,29 @@ paths: type: number title: End Ts responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/vod/{camera_name}/start/{start_ts}/end/{end_ts}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{camera_name}/start/{start_ts}/end/{end_ts}: get: tags: - Media summary: Vod Ts - description: >- - Returns an HLS playlist for the specified timestamp-range on the - specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified timestamp-range on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. operationId: vod_ts_vod__camera_name__start__start_ts__end__end_ts__get parameters: - name: camera_name @@ -4820,7 +5673,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -4842,26 +5695,31 @@ paths: default: false title: Force Discontinuity responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/vod/{year_month}/{day}/{hour}/{camera_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{year_month}/{day}/{hour}/{camera_name}: get: tags: - Media summary: Vod Hour No Timezone - description: >- - Returns an HLS playlist for the specified date-time on the specified - camera. Append /master.m3u8 or /index.m3u8 for HLS playback. - operationId: vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified date-time on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_hour_no_timezone_vod__year_month___day___hour___camera_name__get parameters: - name: year_month in: path @@ -4887,30 +5745,34 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/{year_month}/{day}/{hour}/{camera_name}/{tz_name}: get: tags: - Media summary: Vod Hour - description: >- - Returns an HLS playlist for the specified date-time (with timezone) on - the specified camera. Append /master.m3u8 or /index.m3u8 for HLS - playback. - operationId: vod_hour_vod__year_month___day___hour___camera_name___tz_name__get + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified date-time (with timezone) on the specified camera. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_hour_vod__year_month___day___hour___camera_name___tz_name__get parameters: - name: year_month in: path @@ -4936,7 +5798,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: tz_name in: path @@ -4945,25 +5807,29 @@ paths: type: string title: Tz Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/vod/event/{event_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/event/{event_id}: get: tags: - Media summary: Vod Event - description: >- - Returns an HLS playlist for the specified object. Append /master.m3u8 or - /index.m3u8 for HLS playback. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback. operationId: vod_event_vod_event__event_id__get parameters: - name: event_id @@ -4982,26 +5848,31 @@ paths: title: Padding description: Padding to apply to the vod. responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /vod/clip/{camera_name}/start/{start_ts}/end/{end_ts}: get: tags: - Media summary: Vod Clip - description: >- - Returns an HLS playlist for a timestamp range with HLS discontinuity - enabled. Append /master.m3u8 or /index.m3u8 for HLS playback. - operationId: vod_clip_vod_clip__camera_name__start__start_ts__end__end_ts__get + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns an HLS playlist for a timestamp range with HLS discontinuity enabled. Append /master.m3u8 or /index.m3u8 for HLS playback. + operationId: + vod_clip_vod_clip__camera_name__start__start_ts__end__end_ts__get parameters: - name: camera_name in: path @@ -5009,7 +5880,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -5024,23 +5895,29 @@ paths: type: number title: End Ts responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/snapshot.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /events/{event_id}/snapshot.jpg: get: tags: - Media summary: Event Snapshot - description: Returns a snapshot image for the specified object id. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns a snapshot image for the specified object id. operationId: event_snapshot_events__event_id__snapshot_jpg_get parameters: - name: event_id @@ -5055,7 +5932,7 @@ paths: schema: anyOf: - type: boolean - - type: "null" + - type: 'null' default: false title: Download - name: timestamp @@ -5064,7 +5941,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Timestamp - name: bbox in: query @@ -5072,7 +5949,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Bbox - name: crop in: query @@ -5080,7 +5957,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Crop - name: height in: query @@ -5088,7 +5965,7 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Height - name: quality in: query @@ -5096,21 +5973,24 @@ paths: schema: anyOf: - type: integer - - type: "null" + - type: 'null' title: Quality responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/thumbnail.{extension}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /events/{event_id}/thumbnail.{extension}: get: tags: - Media @@ -5127,7 +6007,7 @@ paths: in: path required: true schema: - $ref: "#/components/schemas/Extension" + $ref: '#/components/schemas/Extension' - name: max_cache_age in: query required: false @@ -5147,27 +6027,23 @@ paths: - android default: ios title: Format - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/grid.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/grid.jpg: get: tags: - Media @@ -5180,7 +6056,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: color in: query @@ -5197,23 +6073,30 @@ paths: default: 0.5 title: Font Scale responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/region_grid": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/region_grid: delete: tags: - Media summary: Clear Region Grid - description: Clear the region grid for a camera. + description: |- + **Access:** Admin role required. + + Clear the region grid for a camera. operationId: clear_region_grid__camera_name__region_grid_delete parameters: - name: camera_name @@ -5223,23 +6106,27 @@ paths: type: string title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/snapshot-clean.webp": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /events/{event_id}/snapshot-clean.webp: get: tags: - Media summary: Event Snapshot Clean - operationId: event_snapshot_clean_events__event_id__snapshot_clean_webp_get + operationId: + event_snapshot_clean_events__event_id__snapshot_clean_webp_get parameters: - name: event_id in: path @@ -5254,27 +6141,23 @@ paths: type: boolean default: false title: Download - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/clip.mp4": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /events/{event_id}/clip.mp4: get: tags: - Media @@ -5296,27 +6179,61 @@ paths: default: 0 title: Padding description: Padding to apply to clip. - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/events/{event_id}/preview.gif": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{review_id}/clip.mp4: + get: + tags: + - Media + summary: Review Clip + operationId: review_clip_review__review_id__clip_mp4_get + parameters: + - name: review_id + in: path + required: true + schema: + type: string + title: Review Id + - name: padding + in: query + required: false + schema: + type: integer + description: Padding to apply to clip. + default: 0 + title: Padding + description: Padding to apply to clip. + responses: + '200': + description: Successful Response + content: + application/json: + schema: {} + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /events/{event_id}/preview.gif: get: tags: - Media @@ -5329,32 +6246,29 @@ paths: schema: type: string title: Event Id - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif: get: tags: - Media summary: Preview Gif - operationId: preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get + operationId: + preview_gif__camera_name__start__start_ts__end__end_ts__preview_gif_get parameters: - name: camera_name in: path @@ -5362,7 +6276,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -5386,23 +6300,28 @@ paths: title: Max Cache Age description: Max cache age in seconds. Default 30 days in seconds. responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4: get: tags: - Media summary: Preview Mp4 - operationId: preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get + operationId: + preview_mp4__camera_name__start__start_ts__end__end_ts__preview_mp4_get parameters: - name: camera_name in: path @@ -5410,7 +6329,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: start_ts in: path @@ -5434,18 +6353,22 @@ paths: title: Max Cache Age description: Max cache age in seconds. Default 7 days in seconds. responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/review/{event_id}/preview": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /review/{event_id}/preview: get: tags: - Media @@ -5468,32 +6391,31 @@ paths: - mp4 default: gif title: Format - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/preview/{file_name}/thumbnail.webp": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /preview/{file_name}/thumbnail.webp: get: tags: - Media summary: Preview Thumbnail - description: Get a thumbnail from the cached preview frames. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Get a thumbnail from the cached preview frames. operationId: preview_thumbnail_preview__file_name__thumbnail_webp_get parameters: - name: file_name @@ -5502,32 +6424,30 @@ paths: schema: type: string title: File Name - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/preview/{file_name}/thumbnail.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /preview/{file_name}/thumbnail.jpg: get: tags: - Media summary: Preview Thumbnail - description: Get a thumbnail from the cached preview frames. + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Get a thumbnail from the cached preview frames. operationId: preview_thumbnail_preview__file_name__thumbnail_jpg_get parameters: - name: file_name @@ -5536,27 +6456,22 @@ paths: schema: type: string title: File Name - - name: camera_name - in: query - required: false - schema: - anyOf: - - type: string - - type: "null" - title: Camera Name responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/{label}/thumbnail.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/{label}/thumbnail.jpg: get: tags: - Media @@ -5569,7 +6484,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: label in: path @@ -5578,18 +6493,22 @@ paths: type: string title: Label responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/{label}/best.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/best.jpg: get: tags: - Media @@ -5602,7 +6521,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: label in: path @@ -5611,18 +6530,22 @@ paths: type: string title: Label responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/{label}/clip.mp4": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/clip.mp4: get: tags: - Media @@ -5635,7 +6558,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: label in: path @@ -5644,25 +6567,30 @@ paths: type: string title: Label responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/{label}/snapshot.jpg": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + description: '**Access:** Authenticated user with access to the referenced camera.' + /{camera_name}/{label}/snapshot.jpg: get: tags: - Media summary: Label Snapshot - description: >- - Returns the snapshot image from the latest event for the given camera - and label combo + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the snapshot image from the latest event for the given camera and label combo operationId: label_snapshot__camera_name___label__snapshot_jpg_get parameters: - name: camera_name @@ -5671,7 +6599,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: label in: path @@ -5680,23 +6608,28 @@ paths: type: string title: Label responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/search/motion": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion: post: tags: - Motion Search summary: Start motion search job description: |- + **Access:** Authenticated user with access to the referenced camera. + Starts an asynchronous search for significant motion changes within a user-defined Region of Interest (ROI) over a specified time range. Returns a job_id that can be used to poll for results. @@ -5708,34 +6641,40 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/MotionSearchRequest" + $ref: '#/components/schemas/MotionSearchRequest' responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/MotionSearchStartResponse" - "422": + $ref: '#/components/schemas/MotionSearchStartResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/search/motion/{job_id}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion/{job_id}: get: tags: - Motion Search summary: Get motion search job status - description: Returns the status and results (if complete) of a motion search job. - operationId: >- + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns the status and results (if complete) of a motion search job. + operationId: get_motion_search_status_endpoint__camera_name__search_motion__job_id__get parameters: - name: camera_name @@ -5744,7 +6683,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: job_id in: path @@ -5753,25 +6692,31 @@ paths: type: string title: Job Id responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/MotionSearchStatusResponse" - "422": + $ref: '#/components/schemas/MotionSearchStatusResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/search/motion/{job_id}/cancel": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/search/motion/{job_id}/cancel: post: tags: - Motion Search summary: Cancel motion search job - description: Cancels an active motion search job if it is still processing. - operationId: >- + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Cancels an active motion search job if it is still processing. + operationId: cancel_motion_search_endpoint__camera_name__search_motion__job_id__cancel_post parameters: - name: camera_name @@ -5780,7 +6725,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: job_id in: path @@ -5789,17 +6734,20 @@ paths: type: string title: Job Id responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera /recordings/storage: get: tags: @@ -5807,17 +6755,24 @@ paths: summary: Get Recordings Storage Usage operationId: get_recordings_storage_usage_recordings_storage_get responses: - "200": + '200': description: Successful Response content: application/json: schema: {} + security: + - frigateAdminAuth: [] + x-required-role: admin + description: '**Access:** Admin role required.' /recordings/summary: get: tags: - Recordings summary: All Recordings Summary - description: Returns true/false by day indicating if recordings exist + description: |- + **Access:** Any authenticated user. + + Returns true/false by day indicating if recordings exist operationId: all_recordings_summary_recordings_summary_get parameters: - name: timezone @@ -5833,27 +6788,33 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/recordings/summary": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /{camera_name}/recordings/summary: get: tags: - Recordings summary: Recordings Summary - description: Returns hourly summary for recordings of given camera + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Returns hourly summary for recordings of given camera operationId: recordings_summary__camera_name__recordings_summary_get parameters: - name: camera_name @@ -5862,7 +6823,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: timezone in: query @@ -5872,25 +6833,29 @@ paths: default: utc title: Timezone responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/{camera_name}/recordings": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera + /{camera_name}/recordings: get: tags: - Recordings summary: Recordings - description: >- - Return specific camera recordings between the given 'after'/'end' times. - If not provided the last hour will be used + description: |- + **Access:** Authenticated user with access to the referenced camera. + + Return specific camera recordings between the given 'after'/'end' times. If not provided the last hour will be used operationId: recordings__camera_name__recordings_get parameters: - name: camera_name @@ -5899,40 +6864,44 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera Name - name: after in: query required: false schema: type: number - default: 1774023877.74743 title: After - name: before in: query required: false schema: type: number - default: 1774027477.74744 title: Before responses: - "200": + '200': description: Successful Response content: application/json: schema: {} - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: camera /recordings/unavailable: get: tags: - Recordings summary: No Recordings - description: Get time ranges with no recordings. + description: |- + **Access:** Any authenticated user. + + Get time ranges with no recordings. operationId: no_recordings_recordings_unavailable_get parameters: - name: cameras @@ -5962,7 +6931,7 @@ paths: default: 30 title: Scale responses: - "200": + '200': description: Successful Response content: application/json: @@ -5971,18 +6940,23 @@ paths: items: type: object title: Response No Recordings Recordings Unavailable Get - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" - "/recordings/start/{start}/end/{end}": + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateUserAuth: [] + x-required-role: any + /recordings/start/{start}/end/{end}: delete: tags: - Recordings summary: Delete recordings description: |- + **Access:** Admin role required. + Deletes recordings within the specified time range. Recordings can be filtered by cameras and kept based on motion, objects, or audio attributes. operationId: delete_recordings_recordings_start__start__end__end__delete @@ -6009,7 +6983,7 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' title: Keep - name: cameras in: query @@ -6017,83 +6991,140 @@ paths: schema: anyOf: - type: string - - type: "null" + - type: 'null' default: all title: Cameras responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/GenericResponse" - "422": + $ref: '#/components/schemas/GenericResponse' + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /debug_replay/start: post: tags: - App summary: Start debug replay - description: - Start a debug replay session from camera recordings. Returns - immediately while clip generation runs as a background job; subscribe - to the 'debug_replay' job_state WS topic to track progress. + description: |- + **Access:** Admin role required. + + Start a debug replay session from camera recordings. Returns immediately while clip generation runs as a background job; subscribe to the 'debug_replay' job_state WS topic to track progress. operationId: start_debug_replay_debug_replay_start_post requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/DebugReplayStartBody" + $ref: '#/components/schemas/DebugReplayStartBody' responses: - "202": + '202': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/DebugReplayStartResponse" - "400": + $ref: '#/components/schemas/DebugReplayStartResponse' + '400': description: Invalid camera, time range, or no recordings - "409": + '409': description: A replay session is already active - "422": + '422': description: Validation Error content: application/json: schema: - $ref: "#/components/schemas/HTTPValidationError" + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin + /debug_replay/start_from_export: + post: + tags: + - App + summary: Start debug replay from an export + description: |- + **Access:** Admin role required. + + Start a debug replay session covering an existing export's time range. The end time is derived from the export's video duration. + operationId: + start_debug_replay_from_export_debug_replay_start_from_export_post + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartFromExportBody' + responses: + '202': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DebugReplayStartResponse' + '400': + description: Invalid export, time range, or no recordings + '404': + description: Export not found + '409': + description: A replay session is already active + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + security: + - frigateAdminAuth: [] + x-required-role: admin /debug_replay/status: get: tags: - App summary: Get debug replay status - description: Get the status of the current debug replay session. + description: |- + **Access:** Admin role required. + + Get the status of the current debug replay session. operationId: get_debug_replay_status_debug_replay_status_get responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/DebugReplayStatusResponse" + $ref: '#/components/schemas/DebugReplayStatusResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin /debug_replay/stop: post: tags: - App summary: Stop debug replay - description: Stop the active debug replay session and clean up all artifacts. + description: |- + **Access:** Admin role required. + + Stop the active debug replay session and clean up all artifacts. operationId: stop_debug_replay_debug_replay_stop_post responses: - "200": + '200': description: Successful Response content: application/json: schema: - $ref: "#/components/schemas/DebugReplayStopResponse" + $ref: '#/components/schemas/DebugReplayStopResponse' + security: + - frigateAdminAuth: [] + x-required-role: admin components: schemas: AppConfigSetBody: @@ -6105,12 +7136,12 @@ components: update_topic: anyOf: - type: string - - type: "null" + - type: 'null' title: Update Topic config_data: anyOf: - type: object - - type: "null" + - type: 'null' title: Config Data skip_save: type: boolean @@ -6142,7 +7173,7 @@ components: role: anyOf: - type: string - - type: "null" + - type: 'null' title: Role default: viewer type: object @@ -6158,7 +7189,7 @@ components: old_password: anyOf: - type: string - - type: "null" + - type: 'null' title: Old Password type: object required: @@ -6183,6 +7214,155 @@ components: required: - event_id title: AudioTranscriptionBody + BatchExportBody: + properties: + items: + items: + $ref: '#/components/schemas/BatchExportItem' + type: array + maxItems: 50 + minItems: 1 + title: Items + description: List of export items. Each item has its own camera and + time range. + export_case_id: + anyOf: + - type: string + maxLength: 30 + - type: 'null' + title: Export case ID + description: Existing export case ID to assign all exports to. + Attaching to an existing case is temporarily admin-only until + case-level ACLs exist. + new_case_name: + anyOf: + - type: string + maxLength: 100 + - type: 'null' + title: New case name + description: Name of a new export case to create when export_case_id + is omitted + new_case_description: + anyOf: + - type: string + - type: 'null' + title: New case description + description: Optional description for a newly created export case + type: object + required: + - items + title: BatchExportBody + BatchExportItem: + properties: + camera: + type: string + title: Camera name + start_time: + type: number + title: Start time + end_time: + type: number + title: End time + image_path: + anyOf: + - type: string + - type: 'null' + title: Existing thumbnail path + description: Optional existing image to use as the export thumbnail + friendly_name: + anyOf: + - type: string + maxLength: 256 + - type: 'null' + title: Friendly name + description: Optional friendly name for this specific export item + client_item_id: + anyOf: + - type: string + maxLength: 128 + - type: 'null' + title: Client item ID + description: Optional opaque client identifier echoed back in results + type: object + required: + - camera + - start_time + - end_time + title: BatchExportItem + BatchExportResponse: + properties: + export_case_id: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + description: Export case ID associated with the batch + export_ids: + items: + type: string + type: array + title: Export Ids + description: Export IDs successfully queued + results: + items: + $ref: '#/components/schemas/BatchExportResultModel' + type: array + title: Results + description: Per-item batch export results + type: object + required: + - export_ids + - results + title: BatchExportResponse + description: Response model for starting an export batch. + BatchExportResultModel: + properties: + camera: + type: string + title: Camera + description: Camera name for this export attempt + export_id: + anyOf: + - type: string + - type: 'null' + title: Export Id + description: The export ID when the export was successfully queued + success: + type: boolean + title: Success + description: Whether the export was successfully queued + status: + anyOf: + - type: string + - type: 'null' + title: Status + description: Queue status for this camera export + error: + anyOf: + - type: string + - type: 'null' + title: Error + description: Validation or queueing error for this item, if any + item_index: + anyOf: + - type: integer + - type: 'null' + title: Item Index + description: Zero-based index of this result within the request items + list + client_item_id: + anyOf: + - type: string + - type: 'null' + title: Client Item Id + description: Opaque client-supplied item identifier echoed from the + request + type: object + required: + - camera + - success + title: BatchExportResultModel + description: Per-item result for a batch export request. Body_recognize_face_faces_recognize_post: properties: file: @@ -6213,28 +7393,42 @@ components: required: - value title: CameraSetBody + ChaptersEnum: + type: string + enum: + - none + - recording_segments + - review_items + title: ChaptersEnum ChatCompletionRequest: properties: messages: items: - $ref: "#/components/schemas/ChatMessage" + $ref: '#/components/schemas/ChatMessage' type: array title: Messages description: List of messages in the conversation max_tool_iterations: type: integer - maximum: 10 - minimum: 1 + maximum: 10.0 + minimum: 1.0 title: Max Tool Iterations - description: "Maximum number of tool call iterations (default: 5)" + description: 'Maximum number of tool call iterations (default: 5)' default: 5 stream: type: boolean title: Stream - description: >- - If true, stream the final assistant response in the body as - newline-delimited JSON. + description: If true, stream the final assistant response in the body + as newline-delimited JSON. default: false + enable_thinking: + anyOf: + - type: boolean + - type: 'null' + title: Enable Thinking + description: Per-request thinking toggle. None means use the provider + default. Ignored by providers that do not expose a per-request + thinking switch. type: object required: - messages @@ -6247,25 +7441,39 @@ components: title: Role description: "Message role: 'user', 'assistant', 'system', or 'tool'" content: - type: string + anyOf: + - {} + - type: 'null' title: Content - description: Message content + description: Message content. Usually a string, but may be a + multimodal content list (e.g. text + image_url) or null for + assistant turns that only request tool calls. tool_call_id: anyOf: - type: string - - type: "null" + - type: 'null' title: Tool Call Id - description: "For tool messages, the ID of the tool call" + description: For tool messages, the ID of the tool call name: anyOf: - type: string - - type: "null" + - type: 'null' title: Name - description: "For tool messages, the tool name" + description: For tool messages, the tool name + tool_calls: + anyOf: + - items: + type: object + type: array + - type: 'null' + title: Tool Calls + description: For assistant messages replayed from prior turns, the + OpenAI-format tool calls the model previously requested. Replaying + these verbatim keeps the conversation prefix byte-for-byte identical + so the model server's prompt cache hits on follow-up turns. type: object required: - role - - content title: ChatMessage description: A single message in a chat conversation. DayReview: @@ -6312,6 +7520,17 @@ components: - end_time title: DebugReplayStartBody description: Request body for starting a debug replay session. + DebugReplayStartFromExportBody: + properties: + export_id: + type: string + title: Export id + type: object + required: + - export_id + title: DebugReplayStartFromExportBody + description: Request body for starting a debug replay session from an + export. DebugReplayStartResponse: properties: success: @@ -6338,22 +7557,22 @@ components: replay_camera: anyOf: - type: string - - type: "null" + - type: 'null' title: Replay Camera source_camera: anyOf: - type: string - - type: "null" + - type: 'null' title: Source Camera start_time: anyOf: - type: number - - type: "null" + - type: 'null' title: Start Time end_time: anyOf: - type: number - - type: "null" + - type: 'null' title: End Time live_ready: type: boolean @@ -6363,7 +7582,13 @@ components: required: - active title: DebugReplayStatusResponse - description: Response for debug replay status. + description: |- + Response for debug replay status. + + Returns only session-presence fields. Startup progress and error + details flow through the job_state WebSocket topic via the + debug_replay job (see frigate.jobs.debug_replay); the + Replay page subscribes there with useJobStatus("debug_replay"). DebugReplayStopResponse: properties: success: @@ -6435,7 +7660,7 @@ components: sub_label: anyOf: - type: string - - type: "null" + - type: 'null' title: Sub Label camera: type: string @@ -6446,12 +7671,12 @@ components: end_time: anyOf: - type: number - - type: "null" + - type: 'null' title: End Time false_positive: anyOf: - type: boolean - - type: "null" + - type: 'null' title: False Positive zones: items: @@ -6461,7 +7686,7 @@ components: thumbnail: anyOf: - type: string - - type: "null" + - type: 'null' title: Thumbnail has_clip: type: boolean @@ -6475,22 +7700,22 @@ components: plus_id: anyOf: - type: string - - type: "null" + - type: 'null' title: Plus Id model_hash: anyOf: - type: string - - type: "null" + - type: 'null' title: Model Hash detector_type: anyOf: - type: string - - type: "null" + - type: 'null' title: Detector Type model_type: anyOf: - type: string - - type: "null" + - type: 'null' title: Model Type data: type: object @@ -6542,36 +7767,36 @@ components: sub_label: anyOf: - type: string - - type: "null" + - type: 'null' title: Sub Label score: anyOf: - type: number - - type: "null" + - type: 'null' title: Score default: 0 duration: anyOf: - type: integer - - type: "null" + - type: 'null' title: Duration default: 30 include_recording: anyOf: - type: boolean - - type: "null" + - type: 'null' title: Include Recording default: true draw: anyOf: - type: object - - type: "null" + - type: 'null' title: Draw default: {} pre_capture: anyOf: - type: integer - - type: "null" + - type: 'null' title: Pre Capture type: object title: EventsCreateBody @@ -6591,7 +7816,7 @@ components: description: anyOf: - type: string - - type: "null" + - type: 'null' title: The description of the event type: object required: @@ -6602,7 +7827,7 @@ components: end_time: anyOf: - type: number - - type: "null" + - type: 'null' title: End Time type: object title: EventsEndBody @@ -6615,157 +7840,14 @@ components: recognizedLicensePlateScore: anyOf: - type: number - maximum: 1 - exclusiveMinimum: 0 - - type: "null" + maximum: 1.0 + exclusiveMinimum: 0.0 + - type: 'null' title: Score for recognized license plate type: object required: - recognizedLicensePlate title: EventsLPRBody - BatchExportBody: - properties: - items: - items: - $ref: "#/components/schemas/BatchExportItem" - type: array - minItems: 1 - maxItems: 50 - title: Items - description: List of export items. Each item has its own camera and time range. - export_case_id: - anyOf: - - type: string - maxLength: 30 - - type: "null" - title: Export case ID - description: Existing export case ID to assign all exports to. Attaching to an existing case is temporarily admin-only until case-level ACLs exist. - new_case_name: - anyOf: - - type: string - maxLength: 100 - - type: "null" - title: New case name - description: Name of a new export case to create when export_case_id is omitted - new_case_description: - anyOf: - - type: string - - type: "null" - title: New case description - description: Optional description for a newly created export case - type: object - required: - - items - title: BatchExportBody - BatchExportItem: - properties: - camera: - type: string - title: Camera name - start_time: - type: number - title: Start time - end_time: - type: number - title: End time - image_path: - anyOf: - - type: string - - type: "null" - title: Existing thumbnail path - description: Optional existing image to use as the export thumbnail - friendly_name: - anyOf: - - type: string - maxLength: 256 - - type: "null" - title: Friendly name - description: Optional friendly name for this specific export item - client_item_id: - anyOf: - - type: string - maxLength: 128 - - type: "null" - title: Client item ID - description: Optional opaque client identifier echoed back in results - type: object - required: - - camera - - start_time - - end_time - title: BatchExportItem - BatchExportResponse: - properties: - export_case_id: - anyOf: - - type: string - - type: "null" - title: Export Case Id - description: Export case ID associated with the batch - export_ids: - items: - type: string - type: array - title: Export Ids - description: Export IDs successfully queued - results: - items: - $ref: "#/components/schemas/BatchExportResultModel" - type: array - title: Results - description: Per-item batch export results - type: object - required: - - export_ids - - results - title: BatchExportResponse - description: Response model for starting an export batch. - BatchExportResultModel: - properties: - camera: - type: string - title: Camera - description: Camera name for this export attempt - export_id: - anyOf: - - type: string - - type: "null" - title: Export Id - description: The export ID when the export was successfully queued - success: - type: boolean - title: Success - description: Whether the export was successfully queued - status: - anyOf: - - type: string - - type: "null" - title: Status - description: Queue status for this camera export - error: - anyOf: - - type: string - - type: "null" - title: Error - description: Validation or queueing error for this item, if any - item_index: - anyOf: - - type: integer - - type: "null" - title: Item Index - description: Zero-based index of this result within the request items list - client_item_id: - anyOf: - - type: string - - type: "null" - title: Client Item Id - description: Opaque client-supplied item identifier echoed from the request - type: object - required: - - camera - - success - title: BatchExportResultModel - description: Per-item result for a batch export request. EventsSubLabelBody: properties: subLabel: @@ -6775,14 +7857,14 @@ components: subLabelScore: anyOf: - type: number - maximum: 1 - exclusiveMinimum: 0 - - type: "null" + maximum: 1.0 + exclusiveMinimum: 0.0 + - type: 'null' title: Score for sub label camera: anyOf: - type: string - - type: "null" + - type: 'null' title: Camera this object is detected on. type: object required: @@ -6815,9 +7897,10 @@ components: anyOf: - type: string maxLength: 30 - - type: "null" + - type: 'null' title: Export Case Id - description: "Case ID to assign to, or null to unassign from current case" + description: Case ID to assign to, or null to unassign from current + case type: object required: - ids @@ -6833,7 +7916,7 @@ components: description: anyOf: - type: string - - type: "null" + - type: 'null' title: Description description: Optional description of the export case type: object @@ -6854,7 +7937,7 @@ components: description: anyOf: - type: string - - type: "null" + - type: 'null' title: Description description: Optional description of the export case created_at: @@ -6879,18 +7962,101 @@ components: anyOf: - type: string maxLength: 100 - - type: "null" + - type: 'null' title: Name description: Updated friendly name of the export case description: anyOf: - type: string - - type: "null" + - type: 'null' title: Description description: Updated description of the export case type: object title: ExportCaseUpdateBody description: Request body for updating an existing export case. + ExportJobModel: + properties: + id: + type: string + title: Id + description: Unique identifier for the export job + job_type: + type: string + title: Job Type + description: Job type + status: + type: string + title: Status + description: Current job status + camera: + type: string + title: Camera + description: Camera associated with this export job + name: + anyOf: + - type: string + - type: 'null' + title: Name + description: Friendly name for the export + export_case_id: + anyOf: + - type: string + - type: 'null' + title: Export Case Id + description: ID of the export case this export belongs to + request_start_time: + type: number + title: Request Start Time + description: Requested export start time + request_end_time: + type: number + title: Request End Time + description: Requested export end time + start_time: + anyOf: + - type: number + - type: 'null' + title: Start Time + description: Unix timestamp when execution started + end_time: + anyOf: + - type: number + - type: 'null' + title: End Time + description: Unix timestamp when execution completed + error_message: + anyOf: + - type: string + - type: 'null' + title: Error Message + description: Error message for failed jobs + results: + anyOf: + - type: object + - type: 'null' + title: Results + description: Result metadata for completed jobs + current_step: + type: string + title: Current Step + description: Current execution step (queued, preparing, encoding, + encoding_retry, finalizing) + default: queued + progress_percent: + type: number + title: Progress Percent + description: Progress percentage of the current step (0.0 - 100.0) + default: 0.0 + type: object + required: + - id + - job_type + - status + - camera + - request_start_time + - request_end_time + title: ExportJobModel + description: Model representing a queued or running export job. ExportModel: properties: id: @@ -6924,7 +8090,7 @@ components: export_case_id: anyOf: - type: string - - type: "null" + - type: 'null' title: Export Case Id description: ID of the export case this export belongs to type: object @@ -6941,14 +8107,14 @@ components: ExportRecordingsBody: properties: source: - $ref: "#/components/schemas/PlaybackSourceEnum" + $ref: '#/components/schemas/PlaybackSourceEnum' title: Playback source default: recordings name: anyOf: - type: string maxLength: 256 - - type: "null" + - type: 'null' title: Friendly name image_path: type: string @@ -6957,15 +8123,22 @@ components: anyOf: - type: string maxLength: 30 - - type: "null" + - 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: properties: source: - $ref: "#/components/schemas/PlaybackSourceEnum" + $ref: '#/components/schemas/PlaybackSourceEnum' title: Playback source default: recordings name: @@ -6979,31 +8152,28 @@ components: anyOf: - type: string maxLength: 30 - - type: "null" + - type: 'null' title: Export case ID description: ID of the export case to assign this export to ffmpeg_input_args: anyOf: - type: string - - type: "null" + - type: 'null' title: FFmpeg input arguments - description: >- - Custom FFmpeg input arguments. If not provided, defaults to - timelapse input args. + description: Custom FFmpeg input arguments. If not provided, defaults + to timelapse input args. ffmpeg_output_args: anyOf: - type: string - - type: "null" + - type: 'null' title: FFmpeg output arguments - description: >- - Custom FFmpeg output arguments. If not provided, defaults to - timelapse output args. + description: Custom FFmpeg output arguments. If not provided, defaults + to timelapse output args. cpu_fallback: type: boolean title: CPU Fallback - description: >- - If true, retry export without hardware acceleration if the initial - export fails. + description: If true, retry export without hardware acceleration if + the initial export fails. default: false type: object title: ExportRecordingsCustomBody @@ -7034,25 +8204,23 @@ components: score: anyOf: - type: number - - type: "null" + - type: 'null' title: Score description: Confidence score of the recognition (0-1) face_name: anyOf: - type: string - - type: "null" + - type: 'null' title: Face Name description: The recognized face name if successful type: object required: - success title: FaceRecognitionResponse - description: >- + description: |- Response model for face recognition endpoint. - - Returns the result of attempting to recognize a face from an uploaded - image. + Returns the result of attempting to recognize a face from an uploaded image. FacesResponse: additionalProperties: items: @@ -7075,36 +8243,33 @@ components: GenAIProbeBody: properties: provider: - type: string - enum: - - openai - - azure_openai - - gemini - - ollama - - llamacpp - title: Provider - description: GenAI provider to probe + $ref: '#/components/schemas/GenAIProviderEnum' api_key: anyOf: - type: string - - type: "null" - title: API Key - description: API key for the provider (when applicable) + - type: 'null' + title: Api Key base_url: anyOf: - type: string - - type: "null" - title: Base URL - description: Base URL for self-hosted or compatible providers + - type: 'null' + title: Base Url provider_options: type: object title: Provider Options - description: Additional provider-specific options - default: {} type: object required: - provider title: GenAIProbeBody + GenAIProviderEnum: + type: string + enum: + - openai + - azure_openai + - gemini + - ollama + - llamacpp + title: GenAIProviderEnum GenerateObjectExamplesBody: properties: model_name: @@ -7114,7 +8279,8 @@ components: label: type: string title: Label - description: "Object label to collect examples for (e.g., 'person', 'car')" + description: Object label to collect examples for (e.g., 'person', + 'car') type: object required: - model_name @@ -7138,9 +8304,8 @@ components: minItems: 4 type: object title: Cameras - description: >- - Dictionary mapping camera names to normalized crop coordinates in - [x1, y1, x2, y2] format (values 0-1) + description: Dictionary mapping camera names to normalized crop + coordinates in [x1, y1, x2, y2] format (values 0-1) type: object required: - model_name @@ -7163,7 +8328,7 @@ components: properties: detail: items: - $ref: "#/components/schemas/ValidationError" + $ref: '#/components/schemas/ValidationError' type: array title: Detail type: object @@ -7194,28 +8359,26 @@ components: dry_run: type: boolean title: Dry Run - description: "If True, only report orphans without deleting them" + description: If True, only report orphans without deleting them default: true media_types: items: type: string type: array title: Media Types - description: >- - Types of media to sync: 'all', 'event_snapshots', - 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', - 'recordings' + description: "Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', + 'review_thumbnails', 'previews', 'exports', 'recordings'" default: - all force: type: boolean title: Force - description: "If True, bypass safety threshold checks" + description: If True, bypass safety threshold checks default: false verbose: type: boolean title: Verbose - description: "If True, write full orphan file list to /config/media_sync/.txt" + description: If True, write full orphan file list to disk default: false type: object title: MediaSyncBody @@ -7248,7 +8411,7 @@ components: wall_time_seconds: type: number title: Wall Time Seconds - default: 0 + default: 0.0 segments_with_errors: type: integer title: Segments With Errors @@ -7273,21 +8436,22 @@ components: type: array type: array title: Polygon Points - description: "List of [x, y] normalized coordinates (0-1) defining the ROI polygon" + description: List of [x, y] normalized coordinates (0-1) defining the + ROI polygon threshold: type: integer - maximum: 255 - minimum: 1 + maximum: 255.0 + minimum: 1.0 title: Threshold description: Pixel difference threshold (1-255) default: 30 min_area: type: number - maximum: 100 + maximum: 100.0 minimum: 0.1 title: Min Area description: Minimum change area as a percentage of the ROI - default: 5 + default: 5.0 parallel: type: boolean title: Parallel @@ -7295,8 +8459,8 @@ components: default: false max_results: type: integer - maximum: 200 - minimum: 1 + maximum: 200.0 + minimum: 1.0 title: Max Results description: Maximum number of search results to return default: 25 @@ -7355,33 +8519,33 @@ components: results: anyOf: - items: - $ref: "#/components/schemas/MotionSearchResult" + $ref: '#/components/schemas/MotionSearchResult' type: array - - type: "null" + - type: 'null' title: Results total_frames_processed: anyOf: - type: integer - - type: "null" + - type: 'null' title: Total Frames Processed error_message: anyOf: - type: string - - type: "null" + - type: 'null' title: Error Message metrics: anyOf: - - $ref: "#/components/schemas/MotionSearchMetricsResponse" - - type: "null" + - $ref: '#/components/schemas/MotionSearchMetricsResponse' + - type: 'null' scanning_timestamp: anyOf: - type: number - - type: "null" + - type: 'null' title: Scanning Timestamp progress: anyOf: - type: number - - type: "null" + - type: 'null' title: Progress type: object required: @@ -7497,7 +8661,7 @@ components: type: boolean title: Has Been Reviewed severity: - $ref: "#/components/schemas/SeverityEnum" + $ref: '#/components/schemas/SeverityEnum' thumb_path: type: string title: Thumb Path @@ -7517,10 +8681,10 @@ components: ReviewSummaryResponse: properties: last24Hours: - $ref: "#/components/schemas/Last24HoursReview" + $ref: '#/components/schemas/Last24HoursReview' root: additionalProperties: - $ref: "#/components/schemas/DayReview" + $ref: '#/components/schemas/DayReview' type: object title: Root type: object @@ -7547,9 +8711,15 @@ components: export_id: anyOf: - type: string - - type: "null" + - type: 'null' title: Export Id description: The export ID if successfully started + status: + anyOf: + - type: string + - type: 'null' + title: Status + description: Queue status for the export job type: object required: - success @@ -7581,14 +8751,14 @@ components: TriggerEmbeddingBody: properties: type: - $ref: "#/components/schemas/TriggerType" + $ref: '#/components/schemas/TriggerType' data: type: string title: Data threshold: type: number - maximum: 1 - minimum: 0 + maximum: 1.0 + minimum: 0.0 title: Threshold default: 0.5 type: object @@ -7602,6 +8772,36 @@ components: - thumbnail - description title: TriggerType + VLMMonitorRequest: + properties: + camera: + type: string + title: Camera + condition: + type: string + title: Condition + max_duration_minutes: + type: integer + title: Max Duration Minutes + default: 60 + labels: + items: + type: string + type: array + title: Labels + default: [] + zones: + items: + type: string + type: array + title: Zones + default: [] + type: object + required: + - camera + - condition + title: VLMMonitorRequest + description: Request model for starting a VLM watch job. ValidationError: properties: loc: @@ -7623,3 +8823,19 @@ components: - msg - type title: ValidationError + securitySchemes: + frigateAdminAuth: + type: apiKey + in: cookie + name: frigate_token + description: Authenticated session whose resolved role is 'admin'. The + session is established via the JWT cookie issued by POST /login, or via + proxy auth headers (remote-user / remote-role) when Frigate runs behind + an authenticating reverse proxy. + frigateUserAuth: + type: apiKey + in: cookie + name: frigate_token + description: Any authenticated session (role 'viewer' or higher), + established via the JWT cookie issued by POST /login, or via proxy auth + headers when Frigate runs behind an authenticating reverse proxy. diff --git a/frigate/__main__.py b/frigate/__main__.py index f3181e4946..df27b42a27 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -4,7 +4,6 @@ import multiprocessing as mp import signal import sys import threading -from typing import Union import ruamel.yaml from pydantic import ValidationError @@ -54,7 +53,7 @@ def main() -> None: print("*************************************************************\n") # Attempt to get the original config file for line number tracking config_path = find_config_file() - with open(config_path, "r") as f: + with open(config_path) as f: yaml_config = ruamel.yaml.YAML() yaml_config.preserve_quotes = True full_config = yaml_config.load(f) @@ -68,7 +67,7 @@ def main() -> None: try: for i, part in enumerate(error_path): - key: Union[int, str] = ( + key: int | str = ( int(part) if isinstance(part, str) and part.isdigit() else part ) diff --git a/frigate/api/app.py b/frigate/api/app.py index 8ed365fce8..49b555606d 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -12,7 +12,7 @@ from datetime import datetime, timedelta from functools import reduce from io import StringIO from pathlib import Path as FilePath -from typing import Any, Dict, List, Optional +from typing import Any import aiofiles import ruamel.yaml @@ -113,7 +113,7 @@ def version(): @router.get("/stats", dependencies=[Depends(allow_any_authenticated())]) def stats( request: Request, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): stats_data = request.app.stats_emitter.get_latest_stats() @@ -164,7 +164,7 @@ def metrics(request: Request): # Retrieve the latest statistics and update the Prometheus metrics stats = request.app.stats_emitter.get_latest_stats() # query DB for count of events by camera, label - event_counts: List[Dict[str, Any]] = ( + event_counts: list[dict[str, Any]] = ( Event.select(Event.camera, Event.label, fn.Count()) .group_by(Event.camera, Event.label) .dicts() @@ -250,7 +250,7 @@ async def genai_probe(body: GenAIProbeBody): asyncio.to_thread(client.list_models), timeout=_PROBE_OUTER_TIMEOUT_SECONDS, ) - except asyncio.TimeoutError: + except TimeoutError: return JSONResponse( content={"success": False, "message": "Probe timed out"}, ) @@ -374,7 +374,7 @@ def config(request: Request): if model_path: model_json_path = FilePath(model_path).with_suffix(".json") try: - with open(model_json_path, "r") as f: + with open(model_json_path) as f: model_plus_data = json.load(f) config["model"]["plus"] = model_plus_data except FileNotFoundError: @@ -502,7 +502,7 @@ def config_raw(): status_code=404, ) - with open(config_file, "r") as f: + with open(config_file) as f: raw_config = f.read() f.close() @@ -807,7 +807,7 @@ def config_set(request: Request, body: AppConfigSetBody): try: with lock: - with open(config_file, "r") as f: + with open(config_file) as f: old_raw_config = f.read() try: @@ -854,7 +854,7 @@ def config_set(request: Request, body: AppConfigSetBody): update_yaml_file_bulk(config_file, updates) # validate the updated config - with open(config_file, "r") as f: + with open(config_file) as f: new_raw_config = f.read() try: @@ -971,7 +971,11 @@ def config_set(request: Request, body: AppConfigSetBody): content=( { "success": True, - "message": "Config successfully updated, restart to apply", + "message": ( + "Config successfully updated" + if body.requires_restart == 0 + else "Config successfully updated, restart to apply" + ), } ), status_code=200, @@ -1028,16 +1032,16 @@ def nvinfo(): ) async def logs( service: str = Path(enum=["frigate", "nginx", "go2rtc"]), - download: Optional[str] = None, - stream: Optional[bool] = False, - start: Optional[int] = 0, - end: Optional[int] = None, + download: str | None = None, + stream: bool | None = False, + start: int | None = 0, + end: int | None = None, ): """Get logs for the requested service (frigate/nginx/go2rtc)""" def download_logs(service_location: str): try: - file = open(service_location, "r") + file = open(service_location) contents = file.read() file.close() return JSONResponse(jsonable_encoder(contents)) @@ -1052,7 +1056,7 @@ async def logs( """Asynchronously stream log lines.""" buffer = "" try: - async with aiofiles.open(file_path, "r") as file: + async with aiofiles.open(file_path) as file: await file.seek(0, 2) while True: line = await file.readline() @@ -1090,7 +1094,7 @@ async def logs( # For full logs initially try: - async with aiofiles.open(service_location, "r") as file: + async with aiofiles.open(service_location) as file: contents = await file.read() total_lines, log_lines = process_logs(contents, service, start, end) @@ -1231,7 +1235,7 @@ def get_media_sync_status(job_id: str): @router.get("/labels", dependencies=[Depends(allow_any_authenticated())]) def get_labels( camera: str = "", - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: if camera: @@ -1263,8 +1267,8 @@ def get_labels( @router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())]) def get_sub_labels( - split_joined: Optional[int] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: events = ( @@ -1351,8 +1355,8 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False): "/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())] ) def get_recognized_license_plates( - split_joined: Optional[int] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + split_joined: int | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: query = ( @@ -1393,8 +1397,8 @@ def get_recognized_license_plates( def timeline( camera: str = "all", limit: int = 100, - source_id: Optional[str] = None, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + source_id: str | None = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): clauses = [] @@ -1408,20 +1412,20 @@ def timeline( ] if camera != "all": - clauses.append((Timeline.camera == camera)) + clauses.append(Timeline.camera == camera) if source_id: source_ids = [sid.strip() for sid in source_id.split(",")] if len(source_ids) == 1: - clauses.append((Timeline.source_id == source_ids[0])) + clauses.append(Timeline.source_id == source_ids[0]) else: - clauses.append((Timeline.source_id.in_(source_ids))) + clauses.append(Timeline.source_id.in_(source_ids)) # Enforce per-camera access control - clauses.append((Timeline.camera << allowed_cameras)) + clauses.append(Timeline.camera << allowed_cameras) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) timeline = ( Timeline.select(*selected_columns) @@ -1437,7 +1441,7 @@ def timeline( @router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())]) def hourly_timeline( params: AppTimelineHourlyQueryParameters = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get hourly summary for timeline.""" cameras = params.cameras @@ -1454,23 +1458,23 @@ def hourly_timeline( if cameras != "all": camera_list = cameras.split(",") - clauses.append((Timeline.camera << camera_list)) + clauses.append(Timeline.camera << camera_list) # Enforce per-camera access control - clauses.append((Timeline.camera << allowed_cameras)) + clauses.append(Timeline.camera << allowed_cameras) if labels != "all": label_list = labels.split(",") - clauses.append((Timeline.data["label"] << label_list)) + clauses.append(Timeline.data["label"] << label_list) if before: - clauses.append((Timeline.timestamp < before)) + clauses.append(Timeline.timestamp < before) if after: - clauses.append((Timeline.timestamp > after)) + clauses.append(Timeline.timestamp > after) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) timeline = ( Timeline.select( diff --git a/frigate/api/auth.py b/frigate/api/auth.py index eca51df1a4..204702e3ac 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -11,7 +11,7 @@ import secrets import time from datetime import datetime from pathlib import Path -from typing import List, Optional +from urllib.parse import parse_qs, urlparse from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import JSONResponse, RedirectResponse @@ -26,7 +26,11 @@ from frigate.api.defs.request.app_body import ( AppPutRoleBody, ) from frigate.api.defs.tags import Tags -from frigate.api.media_auth import check_camera_access, deny_response_for_media_uri +from frigate.api.media_auth import ( + check_camera_access, + deny_response_for_media_uri, + is_role_restricted, +) from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM from frigate.models import User @@ -249,7 +253,14 @@ rateLimiter = RateLimiter() def get_remote_addr(request: Request): - route = list(reversed(request.headers.get("x-forwarded-for").split(","))) + # fall back to the direct TCP peer when no proxy chain is present + direct_addr = request.client.host if request.client else None + + forwarded_for = request.headers.get("x-forwarded-for") + if not forwarded_for: + return direct_addr or "127.0.0.1" + + route = list(reversed(forwarded_for.split(","))) logger.debug(f"IP Route: {[r for r in route]}") trusted_proxies = [] for proxy in request.app.frigate_config.auth.trusted_proxies: @@ -286,13 +297,8 @@ def get_remote_addr(request: Request): logger.debug(f"First untrusted IP: {str(ip)}") return str(ip) - # if there wasn't anything in the route, just return the default - remote_addr = None - - if hasattr(request, "remote_addr"): - remote_addr = request.remote_addr - - return remote_addr or "127.0.0.1" + # every hop in the route was trusted, so fall back to the direct peer + return direct_addr or "127.0.0.1" def _cleanup_first_load_seen() -> None: @@ -383,7 +389,7 @@ def verify_password(password, password_hash): return secrets.compare_digest(password_hash, compare_hash) -def validate_password_strength(password: str) -> tuple[bool, Optional[str]]: +def validate_password_strength(password: str) -> tuple[bool, str | None]: """ Validate password strength. @@ -409,13 +415,19 @@ def create_encoded_jwt(user, role, expiration, secret): ) -def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure): +def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure): # TODO: ideally this would set secure as well, but that requires TLS + # SameSite is intentionally left unset (browsers default to Lax). Setting + # SameSite=Lax/Strict would stop the cookie from being sent in cross-origin + # iframes, breaking embedded views such as the Home Assistant Frigate card. + # CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which + # cross-origin pages cannot set without a CORS preflight that Frigate never + # grants (see check_csrf in api/fastapi_app.py). response.set_cookie( key=cookie_name, value=encoded_jwt, httponly=True, - expires=expiration, + max_age=max_age, secure=secure, ) @@ -432,7 +444,7 @@ async def get_current_user(request: Request): return {"username": username, "role": role} -def require_role(required_roles: List[str]): +def require_role(required_roles: list[str]): async def role_checker(request: Request): proxy_config: ProxyConfig = request.app.frigate_config.proxy config_roles = list(request.app.frigate_config.auth.roles.keys()) @@ -658,6 +670,10 @@ def auth(request: Request): if deny_status is not None: return Response("", status_code=deny_status) + deny_status = deny_response_for_go2rtc_stream(original_url, role, request) + if deny_status is not None: + return Response("", status_code=deny_status) + return success_response # now apply authentication @@ -746,7 +762,7 @@ def auth(request: Request): success_response, JWT_COOKIE_NAME, new_encoded_jwt, - new_expiration, + JWT_SESSION_LENGTH, JWT_COOKIE_SECURE, ) @@ -757,6 +773,10 @@ def auth(request: Request): if deny_status is not None: return Response("", status_code=deny_status) + deny_status = deny_response_for_go2rtc_stream(original_url, role, request) + if deny_status is not None: + return Response("", status_code=deny_status) + return success_response except Exception as e: logger.error(f"Error parsing jwt: {e}") @@ -855,7 +875,11 @@ def login(request: Request, body: AppPostLoginBody): encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token) response = Response("", 200) set_jwt_cookie( - response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + response, + JWT_COOKIE_NAME, + encoded_jwt, + JWT_SESSION_LENGTH, + JWT_COOKIE_SECURE, ) # Clear admin_first_time_login flag after successful admin login so the # UI stops showing the first-time login documentation link. @@ -1017,7 +1041,11 @@ async def update_password( ) # Set new JWT cookie on response set_jwt_cookie( - response, JWT_COOKIE_NAME, encoded_jwt, expiration, JWT_COOKIE_SECURE + response, + JWT_COOKIE_NAME, + encoded_jwt, + JWT_SESSION_LENGTH, + JWT_COOKIE_SECURE, ) return response @@ -1062,7 +1090,7 @@ async def update_role( async def require_camera_access( - camera_name: Optional[str] = None, + camera_name: str | None = None, request: Request = None, ): """Dependency to enforce camera access based on user role.""" @@ -1112,8 +1140,68 @@ def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]: return owner_cameras +# nginx proxies these paths straight to go2rtc with authentication-only checks +# (see auth_request.conf). Each names the desired stream via the `src` query +# param, so the camera-level check must happen here in the `/auth` subrequest — +# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}` +# endpoint, not these proxied live-stream paths. +GO2RTC_STREAM_PROXY_PATHS = frozenset( + { + "/live/mse/api/ws", + "/live/webrtc/api/ws", + "/api/go2rtc/webrtc", + } +) + + +def deny_response_for_go2rtc_stream( + original_url: str | None, role: str | None, request: Request +) -> int | None: + """Block role-restricted users from go2rtc live streams they cannot access. + + Returns 403 when any `src` stream named in `original_url` resolves to a + camera outside the role's allow-list (or when no `src` is provided on a + stream-proxy path), otherwise None. Mirrors the resolution logic in + `require_go2rtc_stream_access` so substream names map to their owning + camera correctly. + """ + if not original_url: + return None + + parsed = urlparse(original_url) + if parsed.path not in GO2RTC_STREAM_PROXY_PATHS: + return None + + frigate_config = request.app.frigate_config + + # admin and full-access roles (no allow-list) bypass the camera check + if not role or not is_role_restricted(role, frigate_config): + return None + + sources = parse_qs(parsed.query).get("src", []) + if not sources: + # a stream-proxy request naming no stream has nothing legitimate to + # show a restricted user + return 403 + + allowed_cameras = set( + User.get_allowed_cameras( + role, + frigate_config.auth.roles, + set(frigate_config.cameras.keys()), + ) + ) + + # deny if any requested source resolves outside the allow-list + for src in sources: + if not (_get_stream_owner_cameras(request, src) & allowed_cameras): + return 403 + + return None + + async def require_go2rtc_stream_access( - stream_name: Optional[str] = None, + stream_name: str | None = None, request: Request = None, ): """Dependency to enforce go2rtc stream access based on owning camera access.""" diff --git a/frigate/api/camera.py b/frigate/api/camera.py index 339ee33a16..a86c35883f 100644 --- a/frigate/api/camera.py +++ b/frigate/api/camera.py @@ -34,11 +34,15 @@ from frigate.config.camera.updater import ( ) from frigate.config.env import substitute_frigate_vars from frigate.models import User -from frigate.util.builtin import clean_camera_user_pass +from frigate.util.builtin import clean_camera_user_pass, get_record_segment_time from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files from frigate.util.config import find_config_file from frigate.util.image import run_ffmpeg_snapshot -from frigate.util.services import ffprobe_stream, is_restricted_go2rtc_source +from frigate.util.services import ( + analyze_record_keyframes, + ffprobe_stream, + is_restricted_go2rtc_source, +) logger = logging.getLogger(__name__) @@ -70,7 +74,7 @@ def _is_valid_host(host: str) -> bool: @router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())]) async def go2rtc_streams(request: Request): - r = requests.get("http://127.0.0.1:1984/api/streams") + r = await asyncio.to_thread(requests.get, "http://127.0.0.1:1984/api/streams") if not r.ok: logger.error("Failed to fetch streams from go2rtc") return JSONResponse( @@ -143,6 +147,19 @@ def go2rtc_camera_stream(request: Request, stream_name: str): ) def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""): """Add or update a go2rtc stream configuration.""" + if src and is_restricted_go2rtc_source(src): + logger.warning( + "Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)", + stream_name, + ) + return JSONResponse( + content={ + "success": False, + "message": "Restricted stream source type", + }, + status_code=400, + ) + try: params = {"name": stream_name} if src: @@ -362,6 +379,48 @@ def ffprobe(request: Request, paths: str = "", detailed: bool = False): return JSONResponse(content=output) +@router.get("/keyframe_analysis", dependencies=[Depends(require_role(["admin"]))]) +async def keyframe_analysis(request: Request, camera: str = ""): + """Probe a camera's record stream and classify its keyframe spacing. + + Detects smart/+ codecs and long/variable GOPs that degrade recording. + """ + config: FrigateConfig = request.app.frigate_config + + if camera not in config.cameras: + return JSONResponse( + content={"success": False, "message": f"{camera} is not a valid camera."}, + status_code=404, + ) + + camera_config = config.cameras[camera] + + if not camera_config.enabled: + return JSONResponse( + content={"success": False, "message": f"{camera} is not enabled."}, + status_code=404, + ) + + # keyframe spacing only matters when this camera is recording + if not camera_config.record.enabled: + return JSONResponse(content={"severity": "record_disabled"}) + + # recording guarantees an input carries the record role; its index matches + # the "Stream N" numbering the ffprobe endpoint surfaces (same input order) + record_index, record_input = next( + (idx, i) + for idx, i in enumerate(camera_config.ffmpeg.inputs) + if "record" in i.roles + ) + + segment_time = get_record_segment_time(camera_config) + result = await analyze_record_keyframes( + config.ffmpeg, record_input.path, segment_time + ) + result["stream_index"] = record_index + return JSONResponse(content=result) + + @router.get("/ffprobe/snapshot", dependencies=[Depends(require_role(["admin"]))]) def ffprobe_snapshot(request: Request, url: str = "", timeout: int = 10): """Get a snapshot from a stream URL using ffmpeg.""" @@ -1128,14 +1187,14 @@ async def delete_camera( try: with lock: - with open(config_file, "r") as f: + with open(config_file) as f: old_raw_config = f.read() try: yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) - with open(config_file, "r") as f: + with open(config_file) as f: data = yaml.load(f) # Remove camera from config @@ -1164,7 +1223,7 @@ async def delete_camera( with open(config_file, "w") as f: yaml.dump(data, f) - with open(config_file, "r") as f: + with open(config_file) as f: new_raw_config = f.read() try: @@ -1226,7 +1285,8 @@ async def delete_camera( # Best-effort go2rtc stream removal try: - requests.delete( + await asyncio.to_thread( + requests.delete, "http://127.0.0.1:1984/api/streams", params={"src": camera_name}, timeout=5, diff --git a/frigate/api/chat.py b/frigate/api/chat.py index 4e6bdbd3b4..fa4510cf14 100644 --- a/frigate/api/chat.py +++ b/frigate/api/chat.py @@ -7,7 +7,7 @@ import operator import time from datetime import datetime from functools import reduce -from typing import Any, Dict, List, Optional +from typing import Any, Literal import cv2 from fastapi import APIRouter, Body, Depends, HTTPException, Request @@ -37,6 +37,7 @@ from frigate.api.defs.response.chat_response import ( from frigate.api.defs.tags import Tags from frigate.api.event import _build_attribute_filter_clause, events from frigate.config import FrigateConfig +from frigate.config.classification import SemanticSearchModelEnum from frigate.genai.prompts import ( build_chat_system_prompt, get_attribute_classifications, @@ -59,7 +60,7 @@ class ToolExecuteRequest(BaseModel): """Request model for tool execution.""" tool_name: str - arguments: Dict[str, Any] + arguments: dict[str, Any] class VLMMonitorRequest(BaseModel): @@ -68,8 +69,8 @@ class VLMMonitorRequest(BaseModel): camera: str condition: str max_duration_minutes: int = 60 - labels: List[str] = [] - zones: List[str] = [] + labels: list[str] = [] + zones: list[str] = [] @router.get( @@ -86,42 +87,61 @@ def get_tools(request: Request) -> JSONResponse: tools = get_tool_definitions( semantic_search_enabled=semantic_search_enabled, attribute_classifications=attribute_classifications, + embeddings_language=_embeddings_language(config), ) return JSONResponse(content={"tools": tools}) +def _embeddings_language(config: FrigateConfig) -> Literal["english", "multi"]: + """Return the language capability of the configured embeddings model. + + JinaV1 is English-only; every other option (JinaV2 or a GenAI embeddings + provider) handles multiple languages. + """ + if config.semantic_search.model == SemanticSearchModelEnum.jinav1: + return "english" + + return "multi" + + def _resolve_zones( - zones: List[str], + zones: list[str], config: FrigateConfig, - target_cameras: List[str], -) -> List[str]: + target_cameras: list[str], +) -> list[str]: """Map zone names to their canonical config keys, case-insensitively. LLMs frequently echo a user's casing ("Front Yard") instead of the - configured key ("front_yard"). The downstream zone filter is a SQLite GLOB - over the JSON-encoded zones column, which is case-sensitive — so an - unnormalized name silently returns zero matches. Build a lookup over the - relevant cameras' configured zones and substitute when we find a match; - unknown names pass through so behavior matches what the model asked for. + configured key ("front_yard"), or fall back to a zone's friendly name + ("Front Walkway") instead of its ID ("front_walk"). The downstream zone + filter is a SQLite GLOB over the JSON-encoded zones column, which stores + config keys and is case-sensitive — so an unnormalized name silently + returns zero matches. Build a lookup over the relevant cameras' configured + zones, keyed by both the config key and the friendly name, and substitute + when we find a match; unknown names pass through so behavior matches what + the model asked for. """ if not zones: return zones - lookup: Dict[str, str] = {} + lookup: dict[str, str] = {} for camera_id in target_cameras: camera_config = config.cameras.get(camera_id) if camera_config is None: continue - for zone_name in camera_config.zones.keys(): + for zone_name, zone_config in camera_config.zones.items(): lookup.setdefault(zone_name.lower(), zone_name) + lookup.setdefault( + zone_config.get_formatted_name(zone_name).lower(), zone_name + ) return [lookup.get(z.lower(), z) for z in zones] async def _execute_search_objects( request: Request, - arguments: Dict[str, Any], - allowed_cameras: List[str], + arguments: dict[str, Any], + allowed_cameras: list[str], ) -> JSONResponse: """ Execute the search_objects tool. @@ -201,7 +221,7 @@ async def _execute_search_objects( # Return it as-is for the LLM return response except Exception as e: - logger.error(f"Error executing search_objects: {e}", exc_info=True) + logger.exception(f"Error executing search_objects: {e}") return JSONResponse( content={ "success": False, @@ -213,8 +233,8 @@ async def _execute_search_objects( async def _execute_search_objects_semantic( request: Request, - arguments: Dict[str, Any], - allowed_cameras: List[str], + arguments: dict[str, Any], + allowed_cameras: list[str], semantic_query: str, ) -> JSONResponse: """Search objects via fused thumbnail + description embeddings. @@ -263,8 +283,8 @@ async def _execute_search_objects_semantic( limit = int(arguments.get("limit", 25)) limit = max(1, min(limit, 100)) - visual_distances: Dict[str, float] = {} - description_distances: Dict[str, float] = {} + visual_distances: dict[str, float] = {} + description_distances: dict[str, float] = {} try: rows = context.search_thumbnail(semantic_query) visual_distances = {row[0]: row[1] for row in rows} @@ -305,7 +325,7 @@ async def _execute_search_objects_semantic( eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))} - scored: List[tuple[str, float]] = [] + scored: list[tuple[str, float]] = [] for eid in eligible: v_score = ( distance_to_score(visual_distances[eid], context.thumb_stats) @@ -331,9 +351,9 @@ async def _execute_search_objects_semantic( async def _execute_find_similar_objects( request: Request, - arguments: Dict[str, Any], - allowed_cameras: List[str], -) -> Dict[str, Any]: + arguments: dict[str, Any], + allowed_cameras: list[str], +) -> dict[str, Any]: """Execute the find_similar_objects tool. Returns a plain dict (not JSONResponse) so the chat loop can embed it @@ -403,8 +423,8 @@ async def _execute_find_similar_objects( # version (see frigate/embeddings/__init__.py). Mirror the pattern used by # frigate/api/event.py events_search: fetch top-k globally, then intersect # with the structured filters via Peewee. - visual_distances: Dict[str, float] = {} - description_distances: Dict[str, float] = {} + visual_distances: dict[str, float] = {} + description_distances: dict[str, float] = {} try: if similarity_mode in ("visual", "fused"): @@ -462,7 +482,7 @@ async def _execute_find_similar_objects( eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))} # 6. Fuse and rank. - scored: List[tuple[str, float]] = [] + scored: list[tuple[str, float]] = [] for eid in eligible: v_score = ( distance_to_score(visual_distances[eid], context.thumb_stats) @@ -503,7 +523,7 @@ async def _execute_find_similar_objects( async def execute_tool( request: Request, body: ToolExecuteRequest = Body(...), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ) -> JSONResponse: """ Execute a tool function call. @@ -545,8 +565,8 @@ async def execute_tool( async def _execute_get_live_context( request: Request, camera: str, - allowed_cameras: List[str], -) -> Dict[str, Any]: + allowed_cameras: list[str], +) -> dict[str, Any]: # Reject wildcards explicitly so models retry with a real camera name # instead of silently fanning out across every camera. if camera in ("*", "all"): @@ -593,7 +613,7 @@ async def _execute_get_live_context( "stationary": obj_dict.get("stationary", False), } - result: Dict[str, Any] = { + result: dict[str, Any] = { "camera": camera, "timestamp": frame_time, "detections": list(tracked_objects_dict.values()), @@ -611,7 +631,7 @@ async def _execute_get_live_context( return result except Exception as e: - logger.error(f"Error executing get_live_context: {e}", exc_info=True) + logger.exception(f"Error executing get_live_context: {e}") return { "error": "Error getting live context", } @@ -620,8 +640,8 @@ async def _execute_get_live_context( async def _get_live_frame_image_url( request: Request, camera: str, - allowed_cameras: List[str], -) -> Optional[str]: + allowed_cameras: list[str], +) -> str | None: """ Fetch the current live frame for a camera as a base64 data URL. @@ -659,8 +679,8 @@ async def _get_live_frame_image_url( async def _execute_set_camera_state( request: Request, - arguments: Dict[str, Any], -) -> Dict[str, Any]: + arguments: dict[str, Any], +) -> dict[str, Any]: role = request.headers.get("remote-role", "") if "admin" not in [r.strip() for r in role.split(",")]: return {"error": "Admin privileges required to change camera settings."} @@ -699,10 +719,10 @@ async def _execute_set_camera_state( async def _execute_tool_internal( tool_name: str, - arguments: Dict[str, Any], + arguments: dict[str, Any], request: Request, - allowed_cameras: List[str], -) -> Dict[str, Any]: + allowed_cameras: list[str], +) -> dict[str, Any]: """ Internal helper to execute a tool and return the result as a dict. @@ -763,8 +783,8 @@ async def _execute_tool_internal( async def _execute_start_camera_watch( request: Request, - arguments: Dict[str, Any], -) -> Dict[str, Any]: + arguments: dict[str, Any], +) -> dict[str, Any]: camera = arguments.get("camera", "").strip() condition = arguments.get("condition", "").strip() max_duration_minutes = int(arguments.get("max_duration_minutes", 60)) @@ -801,7 +821,7 @@ async def _execute_start_camera_watch( zones=zones, ) except RuntimeError as e: - logger.error("Failed to start VLM watch job: %s", e, exc_info=True) + logger.exception("Failed to start VLM watch job: %s", e) return {"error": "Failed to start VLM watch job."} return { @@ -814,14 +834,14 @@ async def _execute_start_camera_watch( } -def _execute_stop_camera_watch() -> Dict[str, Any]: +def _execute_stop_camera_watch() -> dict[str, Any]: cancelled = stop_vlm_watch_job() if cancelled: return {"success": True, "message": "Watch job cancelled."} return {"success": False, "message": "No active watch job to cancel."} -def _execute_get_profile_status(request: Request) -> Dict[str, Any]: +def _execute_get_profile_status(request: Request) -> dict[str, Any]: """Return profile status including active profile and activation timestamps.""" profile_manager = getattr(request.app, "profile_manager", None) if profile_manager is None: @@ -846,9 +866,9 @@ def _execute_get_profile_status(request: Request) -> Dict[str, Any]: def _execute_get_recap( - arguments: Dict[str, Any], - allowed_cameras: List[str], -) -> Dict[str, Any]: + arguments: dict[str, Any], + allowed_cameras: list[str], +) -> dict[str, Any]: """Fetch review segments with GenAI metadata for a time period.""" from functools import reduce @@ -909,7 +929,7 @@ def _execute_get_recap( .iterator() ) - events: List[Dict[str, Any]] = [] + events: list[dict[str, Any]] = [] for row in rows: data = row.get("data") or {} @@ -920,7 +940,7 @@ def _execute_get_recap( data = {} camera = row["camera"] - event: Dict[str, Any] = { + event: dict[str, Any] = { "camera": camera.replace("_", " ").title(), "severity": row.get("severity", "detection"), } @@ -979,15 +999,15 @@ def _execute_get_recap( return {"events": events} except Exception as e: - logger.error("Error executing get_recap: %s", e, exc_info=True) + logger.exception("Error executing get_recap: %s", e) return {"error": "Failed to fetch recap data."} async def _execute_pending_tools( - pending_tool_calls: List[Dict[str, Any]], + pending_tool_calls: list[dict[str, Any]], request: Request, - allowed_cameras: List[str], -) -> tuple[List[ToolCall], List[Dict[str, Any]], List[Dict[str, Any]]]: + allowed_cameras: list[str], +) -> tuple[list[ToolCall], list[dict[str, Any]], list[dict[str, Any]]]: """ Execute a list of tool calls. @@ -996,9 +1016,9 @@ async def _execute_pending_tools( tool result dicts for conversation, extra messages to inject after tool results — e.g. user messages with images) """ - tool_calls_out: List[ToolCall] = [] - tool_results: List[Dict[str, Any]] = [] - extra_messages: List[Dict[str, Any]] = [] + tool_calls_out: list[ToolCall] = [] + tool_results: list[dict[str, Any]] = [] + extra_messages: list[dict[str, Any]] = [] for tool_call in pending_tool_calls: tool_name = tool_call["name"] tool_args = tool_call.get("arguments") or {} @@ -1072,13 +1092,12 @@ async def _execute_pending_tools( } ) except Exception as e: - logger.error( + logger.exception( "Error executing tool %s (id: %s): %s. Arguments: %s", tool_name, tool_call_id, e, json.dumps(tool_args), - exc_info=True, ) error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"}) tool_calls_out.append( @@ -1106,7 +1125,7 @@ async def _execute_pending_tools( async def chat_completion( request: Request, body: ChatCompletionRequest = Body(...), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """ Chat completion endpoint with tool calling support. @@ -1135,22 +1154,27 @@ async def chat_completion( tools = get_tool_definitions( semantic_search_enabled=semantic_search_enabled, attribute_classifications=attribute_classifications, + embeddings_language=_embeddings_language(config), ) conversation = [] - system_prompt = build_chat_system_prompt( - config=config, - allowed_cameras=allowed_cameras, - semantic_search_enabled=semantic_search_enabled, - attribute_classifications=attribute_classifications, - ) - - conversation.append( - { - "role": "system", - "content": system_prompt, - } - ) + # Build the system message only when the client hasn't already pinned one. + # The first turn has no system message; we generate it (with the current + # timestamp) and return the whole chain so the client persists it. Later + # turns send it back verbatim, freezing the timestamp so the prompt prefix + # stays byte-identical and the model server's prompt cache keeps hitting. + if not body.messages or body.messages[0].role != "system": + conversation.append( + { + "role": "system", + "content": build_chat_system_prompt( + config=config, + allowed_cameras=allowed_cameras, + semantic_search_enabled=semantic_search_enabled, + attribute_classifications=attribute_classifications, + ), + } + ) for msg in body.messages: msg_dict = { @@ -1161,11 +1185,13 @@ async def chat_completion( msg_dict["tool_call_id"] = msg.tool_call_id if msg.name: msg_dict["name"] = msg.name + if msg.tool_calls is not None: + msg_dict["tool_calls"] = msg.tool_calls conversation.append(msg_dict) tool_iterations = 0 - tool_calls: List[ToolCall] = [] + tool_calls: list[ToolCall] = [] max_iterations = body.max_tool_iterations logger.debug( @@ -1175,11 +1201,20 @@ async def chat_completion( # True LLM streaming when client supports it and stream requested if body.stream and hasattr(genai_client, "chat_with_tools_stream"): - stream_tool_calls: List[ToolCall] = [] stream_iterations = 0 async def stream_body_llm(): - nonlocal conversation, stream_tool_calls, stream_iterations + nonlocal conversation, stream_iterations + + def _emit_chain(extra: list[dict[str, Any]] | None = None): + # Return the full conversation (including the system message) so + # the client persists and replays it verbatim next turn. + chain = conversation + (extra or []) + return ( + json.dumps({"type": "messages", "messages": chain}).encode("utf-8") + + b"\n" + ) + while stream_iterations < max_iterations: if await request.is_disconnected(): logger.debug("Client disconnected, stopping chat stream") @@ -1244,31 +1279,33 @@ async def chat_completion( ) return ( - executed_calls, + _executed_calls, tool_results, extra_msgs, ) = await _execute_pending_tools( pending, request, allowed_cameras ) - stream_tool_calls.extend(executed_calls) conversation.extend(tool_results) conversation.extend(extra_msgs) - yield ( - json.dumps( - { - "type": "tool_calls", - "tool_calls": [ - tc.model_dump() for tc in stream_tool_calls - ], - } - ).encode("utf-8") - + b"\n" - ) + # Emit the running chain so the client can render tool + # calls live and replay them verbatim next turn. + yield _emit_chain() break else: + # Streaming never appends the final assistant message + # to the conversation, so add it to the chain. + yield _emit_chain( + extra=[ + { + "role": "assistant", + "content": msg.get("content"), + } + ] + ) yield (json.dumps({"type": "done"}).encode("utf-8") + b"\n") return else: + yield _emit_chain() yield json.dumps({"type": "done"}).encode("utf-8") + b"\n" return StreamingResponse( @@ -1315,19 +1352,15 @@ async def chat_completion( if body.stream: final_reasoning = response.get("reasoning") + chain = list(conversation) + async def stream_body() -> Any: - if tool_calls: - yield ( - json.dumps( - { - "type": "tool_calls", - "tool_calls": [ - tc.model_dump() for tc in tool_calls - ], - } - ).encode("utf-8") - + b"\n" + yield ( + json.dumps({"type": "messages", "messages": chain}).encode( + "utf-8" ) + + b"\n" + ) # Emit the full reasoning trace up front when the # underlying client did not stream it if final_reasoning: @@ -1363,6 +1396,7 @@ async def chat_completion( finish_reason=response.get("finish_reason", "stop"), tool_iterations=tool_iterations, tool_calls=tool_calls, + messages=list(conversation), ).model_dump(), ) @@ -1395,11 +1429,12 @@ async def chat_completion( finish_reason="length", tool_iterations=tool_iterations, tool_calls=tool_calls, + messages=list(conversation), ).model_dump(), ) except Exception as e: - logger.error(f"Error in chat completion: {e}", exc_info=True) + logger.exception(f"Error in chat completion: {e}") return JSONResponse( content={ "error": "An error occurred while processing your request.", @@ -1462,7 +1497,7 @@ async def start_vlm_monitor( username=request.headers.get("remote-user", ""), ) except RuntimeError as e: - logger.error("Failed to start VLM watch job: %s", e, exc_info=True) + logger.exception("Failed to start VLM watch job: %s", e) return JSONResponse( content={"success": False, "message": "Failed to start VLM watch job."}, status_code=409, diff --git a/frigate/api/chat_util.py b/frigate/api/chat_util.py index 743c38e57c..a2f29c75b2 100644 --- a/frigate/api/chat_util.py +++ b/frigate/api/chat_util.py @@ -9,8 +9,9 @@ loop state — all inputs and outputs are plain data. import logging import math import time +from collections.abc import Generator from datetime import datetime -from typing import Any, Dict, Generator, List, Optional +from typing import Any from frigate.embeddings.util import ZScoreNormalization from frigate.models import Event @@ -30,7 +31,7 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No if not content: return words = content.split(" ") - current: List[str] = [] + current: list[str] = [] current_len = 0 for w in words: current.append(w) @@ -44,8 +45,8 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No def format_events_with_local_time( - events_list: List[Dict[str, Any]], -) -> List[Dict[str, Any]]: + events_list: list[dict[str, Any]], +) -> list[dict[str, Any]]: """Add human-readable local start/end times to each event for the LLM.""" result = [] for evt in events_list: @@ -84,9 +85,9 @@ def distance_to_score(distance: float, stats: ZScoreNormalization) -> float: def fuse_scores( - visual_score: Optional[float], - description_score: Optional[float], -) -> Optional[float]: + visual_score: float | None, + description_score: float | None, +) -> float | None: """Weighted fusion of visual and description similarity scores. If one side is missing (e.g., no description embedding for this event), @@ -102,7 +103,7 @@ def fuse_scores( return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score -def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]: +def parse_iso_to_timestamp(value: str | None) -> float | None: """Parse an ISO-8601 string as server-local time -> unix timestamp. Mirrors the parsing _execute_search_objects uses so both tools accept the @@ -119,9 +120,9 @@ def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]: return None -def hydrate_event(event: Event, score: Optional[float] = None) -> Dict[str, Any]: +def hydrate_event(event: Event, score: float | None = None) -> dict[str, Any]: """Convert an Event row into the dict shape returned by find_similar_objects.""" - data: Dict[str, Any] = { + data: dict[str, Any] = { "id": event.id, "camera": event.camera, "label": event.label, diff --git a/frigate/api/defs/query/app_query_parameters.py b/frigate/api/defs/query/app_query_parameters.py index e182a6afda..626d39679b 100644 --- a/frigate/api/defs/query/app_query_parameters.py +++ b/frigate/api/defs/query/app_query_parameters.py @@ -1,12 +1,10 @@ -from typing import Optional - from pydantic import BaseModel class AppTimelineHourlyQueryParameters(BaseModel): - cameras: Optional[str] = "all" - labels: Optional[str] = "all" - after: Optional[float] = None - before: Optional[float] = None - limit: Optional[int] = 200 - timezone: Optional[str] = "utc" + cameras: str | None = "all" + labels: str | None = "all" + after: float | None = None + before: float | None = None + limit: int | None = 200 + timezone: str | None = "utc" diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index 8e5a5391a9..06d0dfc3af 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -1,28 +1,26 @@ -from typing import Optional - from pydantic import BaseModel, Field DEFAULT_TIME_RANGE = "00:00,24:00" class EventsQueryParams(BaseModel): - camera: Optional[str] = "all" - cameras: Optional[str] = "all" - label: Optional[str] = "all" - labels: Optional[str] = "all" - sub_label: Optional[str] = "all" - sub_labels: Optional[str] = "all" - attributes: Optional[str] = "all" - zone: Optional[str] = "all" - zones: Optional[str] = "all" - limit: Optional[int] = 100 - after: Optional[float] = None - before: Optional[float] = None - time_range: Optional[str] = DEFAULT_TIME_RANGE - has_clip: Optional[int] = None - has_snapshot: Optional[int] = None - in_progress: Optional[int] = None - include_thumbnails: Optional[int] = Field( + camera: str | None = "all" + cameras: str | None = "all" + label: str | None = "all" + labels: str | None = "all" + sub_label: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zone: str | None = "all" + zones: str | None = "all" + limit: int | None = 100 + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: int | None = None + has_snapshot: int | None = None + in_progress: int | None = None + include_thumbnails: int | None = Field( 1, description=( "Deprecated. Thumbnail data is no longer included in the response. " @@ -30,25 +28,25 @@ class EventsQueryParams(BaseModel): ), deprecated=True, ) - favorites: Optional[int] = None - min_score: Optional[float] = None - max_score: Optional[float] = None - min_speed: Optional[float] = None - max_speed: Optional[float] = None - recognized_license_plate: Optional[str] = "all" - is_submitted: Optional[int] = None - min_length: Optional[float] = None - max_length: Optional[float] = None - event_id: Optional[str] = None - sort: Optional[str] = None - timezone: Optional[str] = "utc" + favorites: int | None = None + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + is_submitted: int | None = None + min_length: float | None = None + max_length: float | None = None + event_id: str | None = None + sort: str | None = None + timezone: str | None = "utc" class EventsSearchQueryParams(BaseModel): - query: Optional[str] = None - event_id: Optional[str] = None - search_type: Optional[str] = "thumbnail" - include_thumbnails: Optional[int] = Field( + query: str | None = None + event_id: str | None = None + search_type: str | None = "thumbnail" + include_thumbnails: int | None = Field( 1, description=( "Deprecated. Thumbnail data is no longer included in the response. " @@ -56,28 +54,28 @@ class EventsSearchQueryParams(BaseModel): ), deprecated=True, ) - limit: Optional[int] = 50 - cameras: Optional[str] = "all" - labels: Optional[str] = "all" - sub_labels: Optional[str] = "all" - attributes: Optional[str] = "all" - zones: Optional[str] = "all" - after: Optional[float] = None - before: Optional[float] = None - time_range: Optional[str] = DEFAULT_TIME_RANGE - has_clip: Optional[bool] = None - has_snapshot: Optional[bool] = None - is_submitted: Optional[bool] = None - timezone: Optional[str] = "utc" - min_score: Optional[float] = None - max_score: Optional[float] = None - min_speed: Optional[float] = None - max_speed: Optional[float] = None - recognized_license_plate: Optional[str] = "all" - sort: Optional[str] = None + limit: int | None = 50 + cameras: str | None = "all" + labels: str | None = "all" + sub_labels: str | None = "all" + attributes: str | None = "all" + zones: str | None = "all" + after: float | None = None + before: float | None = None + time_range: str | None = DEFAULT_TIME_RANGE + has_clip: bool | None = None + has_snapshot: bool | None = None + is_submitted: bool | None = None + timezone: str | None = "utc" + min_score: float | None = None + max_score: float | None = None + min_speed: float | None = None + max_speed: float | None = None + recognized_license_plate: str | None = "all" + sort: str | None = None class EventsSummaryQueryParams(BaseModel): - timezone: Optional[str] = "utc" - has_clip: Optional[int] = None - has_snapshot: Optional[int] = None + timezone: str | None = "utc" + has_clip: int | None = None + has_snapshot: int | None = None diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py index 2d4fae1760..f115685970 100644 --- a/frigate/api/defs/query/media_query_parameters.py +++ b/frigate/api/defs/query/media_query_parameters.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import BaseModel @@ -17,33 +16,33 @@ class Extension(str, Enum): class MediaLatestFrameQueryParams(BaseModel): - bbox: Optional[int] = None - timestamp: Optional[int] = None - zones: Optional[int] = None - mask: Optional[int] = None - motion: Optional[int] = None - paths: Optional[int] = None - regions: Optional[int] = None - quality: Optional[int] = 70 - height: Optional[int] = None - store: Optional[int] = None + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + paths: int | None = None + regions: int | None = None + quality: int | None = 70 + height: int | None = None + store: int | None = None class MediaEventsSnapshotQueryParams(BaseModel): - download: Optional[bool] = False - timestamp: Optional[int] = None - bbox: Optional[int] = None - crop: Optional[int] = None - height: Optional[int] = None - quality: Optional[int] = None + download: bool | None = False + timestamp: int | None = None + bbox: int | None = None + crop: int | None = None + height: int | None = None + quality: int | None = None class MediaMjpegFeedQueryParams(BaseModel): fps: int = 3 height: int = 360 - bbox: Optional[int] = None - timestamp: Optional[int] = None - zones: Optional[int] = None - mask: Optional[int] = None - motion: Optional[int] = None - regions: Optional[int] = None + bbox: int | None = None + timestamp: int | None = None + zones: int | None = None + mask: int | None = None + motion: int | None = None + regions: int | None = None diff --git a/frigate/api/defs/query/recordings_query_parameters.py b/frigate/api/defs/query/recordings_query_parameters.py index d4f1b0a7be..770da96bb8 100644 --- a/frigate/api/defs/query/recordings_query_parameters.py +++ b/frigate/api/defs/query/recordings_query_parameters.py @@ -1,21 +1,19 @@ -from typing import Optional, Union - from pydantic import BaseModel from pydantic.json_schema import SkipJsonSchema class MediaRecordingsSummaryQueryParams(BaseModel): timezone: str = "utc" - cameras: Optional[str] = "all" + cameras: str | None = "all" class MediaRecordingsAvailabilityQueryParams(BaseModel): cameras: str = "all" - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None scale: int = 30 class RecordingsDeleteQueryParams(BaseModel): - keep: Optional[str] = None - cameras: Optional[str] = "all" + keep: str | None = None + cameras: str | None = "all" diff --git a/frigate/api/defs/query/regenerate_query_parameters.py b/frigate/api/defs/query/regenerate_query_parameters.py index af50ada2c4..20d1016c41 100644 --- a/frigate/api/defs/query/regenerate_query_parameters.py +++ b/frigate/api/defs/query/regenerate_query_parameters.py @@ -1,13 +1,11 @@ -from typing import Optional - from pydantic import BaseModel, Field from frigate.events.types import RegenerateDescriptionEnum class RegenerateQueryParameters(BaseModel): - source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails - force: Optional[bool] = Field( + source: RegenerateDescriptionEnum | None = RegenerateDescriptionEnum.thumbnails + force: bool | None = Field( default=False, description="Force (re)generating the description even if GenAI is disabled for this camera.", ) diff --git a/frigate/api/defs/query/review_query_parameters.py b/frigate/api/defs/query/review_query_parameters.py index 6d01d824d6..b16146a9bc 100644 --- a/frigate/api/defs/query/review_query_parameters.py +++ b/frigate/api/defs/query/review_query_parameters.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import BaseModel from pydantic.json_schema import SkipJsonSchema @@ -10,11 +8,11 @@ class ReviewQueryParams(BaseModel): cameras: str = "all" labels: str = "all" zones: str = "all" - reviewed: Union[int, SkipJsonSchema[None]] = None - limit: Union[int, SkipJsonSchema[None]] = None - severity: Union[SeverityEnum, SkipJsonSchema[None]] = None - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + reviewed: int | SkipJsonSchema[None] = None + limit: int | SkipJsonSchema[None] = None + severity: SeverityEnum | SkipJsonSchema[None] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None class ReviewSummaryQueryParams(BaseModel): @@ -26,6 +24,6 @@ class ReviewSummaryQueryParams(BaseModel): class ReviewActivityMotionQueryParams(BaseModel): cameras: str = "all" - before: Union[float, SkipJsonSchema[None]] = None - after: Union[float, SkipJsonSchema[None]] = None + before: float | SkipJsonSchema[None] = None + after: float | SkipJsonSchema[None] = None scale: int = 30 diff --git a/frigate/api/defs/request/app_body.py b/frigate/api/defs/request/app_body.py index 2c37f6ae4d..b0b85c7ab9 100644 --- a/frigate/api/defs/request/app_body.py +++ b/frigate/api/defs/request/app_body.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +from typing import Any from pydantic import BaseModel, Field @@ -8,26 +8,26 @@ from frigate.config import GenAIProviderEnum class AppConfigSetBody(BaseModel): requires_restart: int = 1 update_topic: str | None = None - config_data: Optional[Dict[str, Any]] = None + config_data: dict[str, Any] | None = None skip_save: bool = False class GenAIProbeBody(BaseModel): provider: GenAIProviderEnum - api_key: Optional[str] = None - base_url: Optional[str] = None - provider_options: Dict[str, Any] = Field(default_factory=dict) + api_key: str | None = None + base_url: str | None = None + provider_options: dict[str, Any] = Field(default_factory=dict) class AppPutPasswordBody(BaseModel): password: str - old_password: Optional[str] = None + old_password: str | None = None class AppPostUsersBody(BaseModel): username: str password: str - role: Optional[str] = "viewer" + role: str | None = "viewer" class AppPostLoginBody(BaseModel): @@ -47,7 +47,7 @@ class MediaSyncBody(BaseModel): dry_run: bool = Field( default=True, description="If True, only report orphans without deleting them" ) - media_types: List[str] = Field( + media_types: list[str] = Field( default=["all"], description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'", ) diff --git a/frigate/api/defs/request/batch_export_body.py b/frigate/api/defs/request/batch_export_body.py index c0863c8857..24078bcc7c 100644 --- a/frigate/api/defs/request/batch_export_body.py +++ b/frigate/api/defs/request/batch_export_body.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from pydantic import BaseModel, Field, model_validator MAX_BATCH_EXPORT_ITEMS = 50 @@ -9,18 +7,18 @@ class BatchExportItem(BaseModel): camera: str = Field(title="Camera name") start_time: float = Field(title="Start time") end_time: float = Field(title="End time") - image_path: Optional[str] = Field( + image_path: str | None = Field( default=None, title="Existing thumbnail path", description="Optional existing image to use as the export thumbnail", ) - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", max_length=256, description="Optional friendly name for this specific export item", ) - client_item_id: Optional[str] = Field( + client_item_id: str | None = Field( default=None, title="Client item ID", max_length=128, @@ -29,13 +27,13 @@ class BatchExportItem(BaseModel): class BatchExportBody(BaseModel): - items: List[BatchExportItem] = Field( + items: list[BatchExportItem] = Field( title="Items", min_length=1, max_length=MAX_BATCH_EXPORT_ITEMS, description="List of export items. Each item has its own camera and time range.", ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, @@ -44,13 +42,13 @@ class BatchExportBody(BaseModel): "existing case is temporarily admin-only until case-level ACLs exist." ), ) - new_case_name: Optional[str] = Field( + new_case_name: str | None = Field( default=None, title="New case name", max_length=100, description="Name of a new export case to create when export_case_id is omitted", ) - new_case_description: Optional[str] = Field( + new_case_description: str | None = Field( default=None, title="New case description", description="Optional description for a newly created export case", diff --git a/frigate/api/defs/request/chat_body.py b/frigate/api/defs/request/chat_body.py index 228781c80b..5ca674fbc8 100644 --- a/frigate/api/defs/request/chat_body.py +++ b/frigate/api/defs/request/chat_body.py @@ -1,6 +1,6 @@ """Chat API request models.""" -from typing import Optional +from typing import Any from pydantic import BaseModel, Field @@ -11,13 +11,29 @@ class ChatMessage(BaseModel): role: str = Field( description="Message role: 'user', 'assistant', 'system', or 'tool'" ) - content: str = Field(description="Message content") - tool_call_id: Optional[str] = Field( + content: Any | None = Field( + default=None, + description=( + "Message content. Usually a string, but may be a multimodal content " + "list (e.g. text + image_url) or null for assistant turns that only " + "request tool calls." + ), + ) + tool_call_id: str | None = Field( default=None, description="For tool messages, the ID of the tool call" ) - name: Optional[str] = Field( + name: str | None = Field( default=None, description="For tool messages, the tool name" ) + tool_calls: list[dict[str, Any]] | None = Field( + default=None, + description=( + "For assistant messages replayed from prior turns, the OpenAI-format " + "tool calls the model previously requested. Replaying these verbatim " + "keeps the conversation prefix byte-for-byte identical so the model " + "server's prompt cache hits on follow-up turns." + ), + ) class ChatCompletionRequest(BaseModel): @@ -36,7 +52,7 @@ class ChatCompletionRequest(BaseModel): default=False, description="If true, stream the final assistant response in the body as newline-delimited JSON.", ) - enable_thinking: Optional[bool] = Field( + enable_thinking: bool | None = Field( default=None, description=( "Per-request thinking toggle. None means use the provider default. " diff --git a/frigate/api/defs/request/classification_body.py b/frigate/api/defs/request/classification_body.py index fb6a7dd0fd..2aa90f4580 100644 --- a/frigate/api/defs/request/classification_body.py +++ b/frigate/api/defs/request/classification_body.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Tuple - from pydantic import BaseModel, Field @@ -12,14 +10,14 @@ class AudioTranscriptionBody(BaseModel): class DeleteFaceImagesBody(BaseModel): - ids: List[str] = Field( + ids: list[str] = Field( description="List of image filenames to delete from the face folder" ) class GenerateStateExamplesBody(BaseModel): model_name: str = Field(description="Name of the classification model") - cameras: Dict[str, Tuple[float, float, float, float]] = Field( + cameras: dict[str, tuple[float, float, float, float]] = Field( description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)" ) diff --git a/frigate/api/defs/request/events_body.py b/frigate/api/defs/request/events_body.py index d844c31ca7..c7920ae897 100644 --- a/frigate/api/defs/request/events_body.py +++ b/frigate/api/defs/request/events_body.py @@ -1,5 +1,3 @@ -from typing import List, Optional, Union - from pydantic import BaseModel, Field from frigate.config.classification import TriggerType @@ -7,49 +5,47 @@ from frigate.config.classification import TriggerType class EventsSubLabelBody(BaseModel): subLabel: str = Field(title="Sub label", max_length=100) - subLabelScore: Optional[float] = Field( + subLabelScore: float | None = Field( title="Score for sub label", default=None, gt=0.0, le=1.0 ) - camera: Optional[str] = Field( - title="Camera this object is detected on.", default=None - ) + camera: str | None = Field(title="Camera this object is detected on.", default=None) class EventsLPRBody(BaseModel): recognizedLicensePlate: str = Field( title="Recognized License Plate", max_length=100 ) - recognizedLicensePlateScore: Optional[float] = Field( + recognizedLicensePlateScore: float | None = Field( title="Score for recognized license plate", default=None, gt=0.0, le=1.0 ) class EventsAttributesBody(BaseModel): - attributes: List[str] = Field( + attributes: list[str] = Field( title="Selected classification attributes for the event", default_factory=list, ) class EventsDescriptionBody(BaseModel): - description: Union[str, None] = Field(title="The description of the event") + description: str | None = Field(title="The description of the event") class EventsCreateBody(BaseModel): - sub_label: Optional[str] = None - score: Optional[float] = 0 - duration: Optional[int] = 30 - include_recording: Optional[bool] = True - draw: Optional[dict] = {} - pre_capture: Optional[int] = None + sub_label: str | None = None + score: float | None = 0 + duration: int | None = 30 + include_recording: bool | None = True + draw: dict | None = {} + pre_capture: int | None = None class EventsEndBody(BaseModel): - end_time: Optional[float] = None + end_time: float | None = None class EventsDeleteBody(BaseModel): - event_ids: List[str] = Field(title="The event IDs to delete") + event_ids: list[str] = Field(title="The event IDs to delete") class SubmitPlusBody(BaseModel): diff --git a/frigate/api/defs/request/export_bulk_body.py b/frigate/api/defs/request/export_bulk_body.py index 004c67d90c..07283f81d4 100644 --- a/frigate/api/defs/request/export_bulk_body.py +++ b/frigate/api/defs/request/export_bulk_body.py @@ -1,7 +1,5 @@ """Request bodies for bulk export operations.""" -from typing import Optional - from pydantic import BaseModel, Field, conlist, constr @@ -17,7 +15,7 @@ class ExportBulkReassignBody(BaseModel): # List of export IDs with at least one element and each element with at least one char ids: conlist(constr(min_length=1), min_length=1) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, max_length=30, description="Case ID to assign to, or null to unassign from current case", diff --git a/frigate/api/defs/request/export_case_body.py b/frigate/api/defs/request/export_case_body.py index 66cba58ea0..c7b436f14b 100644 --- a/frigate/api/defs/request/export_case_body.py +++ b/frigate/api/defs/request/export_case_body.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, Field @@ -7,7 +5,7 @@ class ExportCaseCreateBody(BaseModel): """Request body for creating a new export case.""" name: str = Field(max_length=100, description="Friendly name of the export case") - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Optional description of the export case" ) @@ -15,11 +13,11 @@ class ExportCaseCreateBody(BaseModel): class ExportCaseUpdateBody(BaseModel): """Request body for updating an existing export case.""" - name: Optional[str] = Field( + name: str | None = Field( default=None, max_length=100, description="Updated friendly name of the export case", ) - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Updated description of the export case" ) diff --git a/frigate/api/defs/request/export_recordings_body.py b/frigate/api/defs/request/export_recordings_body.py index 96ecccaa4c..beb8f39962 100644 --- a/frigate/api/defs/request/export_recordings_body.py +++ b/frigate/api/defs/request/export_recordings_body.py @@ -1,23 +1,32 @@ -from typing import Optional, Union - from pydantic import BaseModel, Field from pydantic.json_schema import SkipJsonSchema -from frigate.record.export import PlaybackSourceEnum +from frigate.record.export import ( + ChaptersEnum, + PlaybackSourceEnum, +) class ExportRecordingsBody(BaseModel): source: PlaybackSourceEnum = Field( default=PlaybackSourceEnum.recordings, title="Playback source" ) - name: Optional[str] = Field(title="Friendly name", default=None, max_length=256) - image_path: Union[str, SkipJsonSchema[None]] = None - export_case_id: Optional[str] = Field( + name: str | None = Field(title="Friendly name", default=None, max_length=256) + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, description="ID of the export case to assign this export to", ) + chapters: ChaptersEnum | None = 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): @@ -25,19 +34,19 @@ class ExportRecordingsCustomBody(BaseModel): default=PlaybackSourceEnum.recordings, title="Playback source" ) name: str = Field(title="Friendly name", default=None, max_length=256) - image_path: Union[str, SkipJsonSchema[None]] = None - export_case_id: Optional[str] = Field( + image_path: str | SkipJsonSchema[None] = None + export_case_id: str | None = Field( default=None, title="Export case ID", max_length=30, description="ID of the export case to assign this export to", ) - ffmpeg_input_args: Optional[str] = Field( + ffmpeg_input_args: str | None = Field( default=None, title="FFmpeg input arguments", description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.", ) - ffmpeg_output_args: Optional[str] = Field( + ffmpeg_output_args: str | None = Field( default=None, title="FFmpeg output arguments", description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.", diff --git a/frigate/api/defs/response/chat_response.py b/frigate/api/defs/response/chat_response.py index c2b3e6b1f2..3007c4c0f4 100644 --- a/frigate/api/defs/response/chat_response.py +++ b/frigate/api/defs/response/chat_response.py @@ -1,6 +1,6 @@ """Chat API response models.""" -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field @@ -17,14 +17,14 @@ class ChatMessageResponse(BaseModel): """A message in the chat response.""" role: str = Field(description="Message role") - content: Optional[str] = Field( + content: str | None = Field( default=None, description="Message content (None if tool calls present)" ) - reasoning: Optional[str] = Field( + reasoning: str | None = Field( default=None, description="Separated reasoning/thinking trace if the model emitted one", ) - tool_calls: Optional[list[ToolCallInvocation]] = Field( + tool_calls: list[ToolCallInvocation] | None = Field( default=None, description="Tool calls if LLM wants to call tools" ) @@ -56,3 +56,12 @@ class ChatCompletionResponse(BaseModel): default_factory=list, description="List of tool calls that were executed during this completion", ) + messages: list[dict[str, Any]] = Field( + default_factory=list, + description=( + "The full conversation chain, including the system message. Persist " + "and replay this verbatim on the next request so the prompt prefix " + "stays byte-identical and the model server's prompt cache keeps " + "hitting." + ), + ) diff --git a/frigate/api/defs/response/classification_response.py b/frigate/api/defs/response/classification_response.py index 92d354f242..09cdfbc176 100644 --- a/frigate/api/defs/response/classification_response.py +++ b/frigate/api/defs/response/classification_response.py @@ -1,9 +1,7 @@ -from typing import Dict, List, Optional - from pydantic import BaseModel, Field, RootModel -class FacesResponse(RootModel[Dict[str, List[str]]]): +class FacesResponse(RootModel[dict[str, list[str]]]): """Response model for the get_faces endpoint. Returns a mapping of face names to lists of image filenames. @@ -17,7 +15,7 @@ class FacesResponse(RootModel[Dict[str, List[str]]]): } """ - root: Dict[str, List[str]] = Field( + root: dict[str, list[str]] = Field( default_factory=dict, description="Dictionary mapping face names to lists of image filenames", ) @@ -30,9 +28,9 @@ class FaceRecognitionResponse(BaseModel): """ success: bool = Field(description="Whether the face recognition was successful") - score: Optional[float] = Field( + score: float | None = Field( default=None, description="Confidence score of the recognition (0-1)" ) - face_name: Optional[str] = Field( + face_name: str | None = Field( default=None, description="The recognized face name if successful" ) diff --git a/frigate/api/defs/response/event_response.py b/frigate/api/defs/response/event_response.py index 083849706a..a366af8d1e 100644 --- a/frigate/api/defs/response/event_response.py +++ b/frigate/api/defs/response/event_response.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict @@ -6,20 +6,20 @@ from pydantic import BaseModel, ConfigDict class EventResponse(BaseModel): id: str label: str - sub_label: Optional[str] + sub_label: str | None camera: str start_time: float - end_time: Optional[float] - false_positive: Optional[bool] + end_time: float | None + false_positive: bool | None zones: list[str] - thumbnail: Optional[str] + thumbnail: str | None has_clip: bool has_snapshot: bool retain_indefinitely: bool - plus_id: Optional[str] - model_hash: Optional[str] - detector_type: Optional[str] - model_type: Optional[str] + plus_id: str | None + model_hash: str | None + detector_type: str | None + model_type: str | None data: dict[str, Any] model_config = ConfigDict(protected_namespaces=()) diff --git a/frigate/api/defs/response/export_case_response.py b/frigate/api/defs/response/export_case_response.py index 713e166837..199d5b6b76 100644 --- a/frigate/api/defs/response/export_case_response.py +++ b/frigate/api/defs/response/export_case_response.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from pydantic import BaseModel, Field @@ -8,7 +6,7 @@ class ExportCaseModel(BaseModel): id: str = Field(description="Unique identifier for the export case") name: str = Field(description="Friendly name of the export case") - description: Optional[str] = Field( + description: str | None = Field( default=None, description="Optional description of the export case" ) created_at: float = Field( @@ -19,4 +17,4 @@ class ExportCaseModel(BaseModel): ) -ExportCasesResponse = List[ExportCaseModel] +ExportCasesResponse = list[ExportCaseModel] diff --git a/frigate/api/defs/response/export_response.py b/frigate/api/defs/response/export_response.py index 10b4a7e643..5e900599d3 100644 --- a/frigate/api/defs/response/export_response.py +++ b/frigate/api/defs/response/export_response.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any from pydantic import BaseModel, Field @@ -15,7 +15,7 @@ class ExportModel(BaseModel): in_progress: bool = Field( description="Whether the export is currently being processed" ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="ID of the export case this export belongs to" ) @@ -25,10 +25,10 @@ class StartExportResponse(BaseModel): success: bool = Field(description="Whether the export was started successfully") message: str = Field(description="Status or error message") - export_id: Optional[str] = Field( + export_id: str | None = Field( default=None, description="The export ID if successfully started" ) - status: Optional[str] = Field( + status: str | None = Field( default=None, description="Queue status for the export job", ) @@ -38,24 +38,24 @@ class BatchExportResultModel(BaseModel): """Per-item result for a batch export request.""" camera: str = Field(description="Camera name for this export attempt") - export_id: Optional[str] = Field( + export_id: str | None = Field( default=None, description="The export ID when the export was successfully queued", ) success: bool = Field(description="Whether the export was successfully queued") - status: Optional[str] = Field( + status: str | None = Field( default=None, description="Queue status for this camera export", ) - error: Optional[str] = Field( + error: str | None = Field( default=None, description="Validation or queueing error for this item, if any", ) - item_index: Optional[int] = Field( + item_index: int | None = Field( default=None, description="Zero-based index of this result within the request items list", ) - client_item_id: Optional[str] = Field( + client_item_id: str | None = Field( default=None, description="Opaque client-supplied item identifier echoed from the request", ) @@ -64,12 +64,12 @@ class BatchExportResultModel(BaseModel): class BatchExportResponse(BaseModel): """Response model for starting an export batch.""" - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="Export case ID associated with the batch", ) - export_ids: List[str] = Field(description="Export IDs successfully queued") - results: List[BatchExportResultModel] = Field( + export_ids: list[str] = Field(description="Export IDs successfully queued") + results: list[BatchExportResultModel] = Field( description="Per-item batch export results" ) @@ -81,29 +81,29 @@ class ExportJobModel(BaseModel): job_type: str = Field(description="Job type") status: str = Field(description="Current job status") camera: str = Field(description="Camera associated with this export job") - name: Optional[str] = Field( + name: str | None = Field( default=None, description="Friendly name for the export", ) - export_case_id: Optional[str] = Field( + export_case_id: str | None = Field( default=None, description="ID of the export case this export belongs to", ) request_start_time: float = Field(description="Requested export start time") request_end_time: float = Field(description="Requested export end time") - start_time: Optional[float] = Field( + start_time: float | None = Field( default=None, description="Unix timestamp when execution started", ) - end_time: Optional[float] = Field( + end_time: float | None = Field( default=None, description="Unix timestamp when execution completed", ) - error_message: Optional[str] = Field( + error_message: str | None = Field( default=None, description="Error message for failed jobs", ) - results: Optional[dict[str, Any]] = Field( + results: dict[str, Any] | None = Field( default=None, description="Result metadata for completed jobs", ) @@ -117,7 +117,7 @@ class ExportJobModel(BaseModel): ) -ExportJobsResponse = List[ExportJobModel] +ExportJobsResponse = list[ExportJobModel] -ExportsResponse = List[ExportModel] +ExportsResponse = list[ExportModel] diff --git a/frigate/api/defs/response/preview_response.py b/frigate/api/defs/response/preview_response.py index d320a865da..70d2cac171 100644 --- a/frigate/api/defs/response/preview_response.py +++ b/frigate/api/defs/response/preview_response.py @@ -1,5 +1,3 @@ -from typing import List - from pydantic import BaseModel, Field @@ -13,5 +11,5 @@ class PreviewModel(BaseModel): end: float = Field(description="Unix timestamp when the preview ends") -PreviewsResponse = List[PreviewModel] -PreviewFramesResponse = List[str] +PreviewsResponse = list[PreviewModel] +PreviewFramesResponse = list[str] diff --git a/frigate/api/defs/response/review_response.py b/frigate/api/defs/response/review_response.py index b2fed3b1a6..a0b755bd60 100644 --- a/frigate/api/defs/response/review_response.py +++ b/frigate/api/defs/response/review_response.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Dict from pydantic import BaseModel, Json @@ -34,7 +33,7 @@ class DayReview(BaseModel): class ReviewSummaryResponse(BaseModel): last24Hours: Last24HoursReview - root: Dict[str, DayReview] + root: dict[str, DayReview] class ReviewActivityMotionResponse(BaseModel): diff --git a/frigate/api/event.py b/frigate/api/event.py index fc7c58c375..d37a547ba4 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -10,7 +10,6 @@ import random import string from functools import reduce from pathlib import Path -from typing import List from urllib.parse import unquote import numpy as np @@ -97,7 +96,7 @@ def _build_attribute_filter_clause(attributes: str): ) def events( params: EventsQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): camera = params.camera cameras = params.cameras @@ -171,7 +170,7 @@ def events( ] if camera != "all": - clauses.append((Event.camera == camera)) + clauses.append(Event.camera == camera) if cameras != "all": requested = set(cameras.split(",")) @@ -181,11 +180,11 @@ def events( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((Event.camera << camera_list)) + clauses.append(Event.camera << camera_list) if labels != "all": label_list = labels.split(",") - clauses.append((Event.label << label_list)) + clauses.append(Event.label << label_list) if sub_labels != "all": # use matching so joined sub labels are included @@ -196,24 +195,24 @@ def events( if "None" in filtered_sub_labels: filtered_sub_labels.remove("None") - sub_label_clauses.append((Event.sub_label.is_null())) + sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: lowered = label.lower() sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) == lowered) + fn.LOWER(Event.sub_label.cast("text")) == lowered ) # include exact matches (case-insensitive) # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*") + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" ) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*") + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" ) sub_label_clause = reduce(operator.or_, sub_label_clauses) - clauses.append((sub_label_clause)) + clauses.append(sub_label_clause) if attributes != "all": # Custom classification results are stored as data[model_name] = result_value @@ -257,19 +256,19 @@ def events( if "None" in filtered_zones: filtered_zones.remove("None") - zone_clauses.append((Event.zones.length() == 0)) + zone_clauses.append(Event.zones.length() == 0) for zone in filtered_zones: - zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') zone_clause = reduce(operator.or_, zone_clauses) - clauses.append((zone_clause)) + clauses.append(zone_clause) if after: - clauses.append((Event.start_time > after)) + clauses.append(Event.start_time > after) if before: - clauses.append((Event.start_time < before)) + clauses.append(Event.start_time < before) if time_range != DEFAULT_TIME_RANGE: # get timezone arg to ensure browser times are used @@ -289,62 +288,60 @@ def events( # should use or operator if time_after > time_before: clauses.append( - ( - reduce( - operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], - ) + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - clauses.append((start_hour_fun > time_after)) - clauses.append((start_hour_fun < time_before)) + clauses.append(start_hour_fun > time_after) + clauses.append(start_hour_fun < time_before) if has_clip is not None: - clauses.append((Event.has_clip == has_clip)) + clauses.append(Event.has_clip == has_clip) if has_snapshot is not None: - clauses.append((Event.has_snapshot == has_snapshot)) + clauses.append(Event.has_snapshot == has_snapshot) if in_progress is not None: - clauses.append((Event.end_time.is_null(in_progress))) + clauses.append(Event.end_time.is_null(in_progress)) if include_thumbnails: selected_columns.append(Event.thumbnail) if favorites: - clauses.append((Event.retain_indefinitely == favorites)) + clauses.append(Event.retain_indefinitely == favorites) if max_score is not None: - clauses.append((Event.data["score"] <= max_score)) + clauses.append(Event.data["score"] <= max_score) if min_score is not None: - clauses.append((Event.data["score"] >= min_score)) + clauses.append(Event.data["score"] >= min_score) if max_speed is not None: - clauses.append((Event.data["average_estimated_speed"] <= max_speed)) + clauses.append(Event.data["average_estimated_speed"] <= max_speed) if min_speed is not None: - clauses.append((Event.data["average_estimated_speed"] >= min_speed)) + clauses.append(Event.data["average_estimated_speed"] >= min_speed) if min_length is not None: - clauses.append(((Event.end_time - Event.start_time) >= min_length)) + clauses.append((Event.end_time - Event.start_time) >= min_length) if max_length is not None: - clauses.append(((Event.end_time - Event.start_time) <= max_length)) + clauses.append((Event.end_time - Event.start_time) <= max_length) if is_submitted is not None: if is_submitted == 0: - clauses.append((Event.plus_id.is_null())) + clauses.append(Event.plus_id.is_null()) elif is_submitted > 0: - clauses.append((Event.plus_id != "")) + clauses.append(Event.plus_id != "") if event_id is not None: - clauses.append((Event.id == event_id)) + clauses.append(Event.id == event_id) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) if sort: if sort == "score_asc": @@ -387,7 +384,7 @@ def events( ) def events_explore( limit: int = 10, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): # get distinct labels for all events distinct_labels = ( @@ -515,7 +512,7 @@ async def event_ids(ids: str, request: Request): def events_search( request: Request, params: EventsSearchQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): query = params.query search_type = params.search_type @@ -595,12 +592,12 @@ def events_search( filtered = requested.intersection(allowed_cameras) if not filtered: return JSONResponse(content=[]) - event_filters.append((Event.camera << list(filtered))) + event_filters.append(Event.camera << list(filtered)) else: - event_filters.append((Event.camera << allowed_cameras)) + event_filters.append(Event.camera << allowed_cameras) if labels != "all": - event_filters.append((Event.label << labels.split(","))) + event_filters.append(Event.label << labels.split(",")) if sub_labels != "all": # use matching so joined sub labels are included @@ -611,23 +608,23 @@ def events_search( if "None" in filtered_sub_labels: filtered_sub_labels.remove("None") - sub_label_clauses.append((Event.sub_label.is_null())) + sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: lowered = label.lower() sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) == lowered) + fn.LOWER(Event.sub_label.cast("text")) == lowered ) # include exact matches (case-insensitive) # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*") + fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" ) sub_label_clauses.append( - (fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*") + fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" ) - event_filters.append((reduce(operator.or_, sub_label_clauses))) + event_filters.append(reduce(operator.or_, sub_label_clauses)) if attributes != "all": # Custom classification results are stored as data[model_name] = result_value @@ -641,12 +638,12 @@ def events_search( if "None" in filtered_zones: filtered_zones.remove("None") - zone_clauses.append((Event.zones.length() == 0)) + zone_clauses.append(Event.zones.length() == 0) for zone in filtered_zones: - zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*')) + zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*') - event_filters.append((reduce(operator.or_, zone_clauses))) + event_filters.append(reduce(operator.or_, zone_clauses)) if recognized_license_plate != "all": filtered_recognized_license_plates = recognized_license_plate.split(",") @@ -674,43 +671,43 @@ def events_search( ) recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates) - event_filters.append((recognized_license_plate_clause)) + event_filters.append(recognized_license_plate_clause) if after: - event_filters.append((Event.start_time > after)) + event_filters.append(Event.start_time > after) if before: - event_filters.append((Event.start_time < before)) + event_filters.append(Event.start_time < before) if has_clip is not None: - event_filters.append((Event.has_clip == has_clip)) + event_filters.append(Event.has_clip == has_clip) if has_snapshot is not None: - event_filters.append((Event.has_snapshot == has_snapshot)) + event_filters.append(Event.has_snapshot == has_snapshot) if is_submitted is not None: if is_submitted == 0: - event_filters.append((Event.plus_id.is_null())) + event_filters.append(Event.plus_id.is_null()) elif is_submitted > 0: - event_filters.append((Event.plus_id != "")) + event_filters.append(Event.plus_id != "") if min_score is not None and max_score is not None: - event_filters.append((Event.data["score"].between(min_score, max_score))) + event_filters.append(Event.data["score"].between(min_score, max_score)) else: if min_score is not None: - event_filters.append((Event.data["score"] >= min_score)) + event_filters.append(Event.data["score"] >= min_score) if max_score is not None: - event_filters.append((Event.data["score"] <= max_score)) + event_filters.append(Event.data["score"] <= max_score) if min_speed is not None and max_speed is not None: event_filters.append( - (Event.data["average_estimated_speed"].between(min_speed, max_speed)) + Event.data["average_estimated_speed"].between(min_speed, max_speed) ) else: if min_speed is not None: - event_filters.append((Event.data["average_estimated_speed"] >= min_speed)) + event_filters.append(Event.data["average_estimated_speed"] >= min_speed) if max_speed is not None: - event_filters.append((Event.data["average_estimated_speed"] <= max_speed)) + event_filters.append(Event.data["average_estimated_speed"] <= max_speed) if time_range != DEFAULT_TIME_RANGE: tz_name = params.timezone @@ -728,17 +725,15 @@ def events_search( # should use or operator if time_after > time_before: event_filters.append( - ( - reduce( - operator.or_, - [(start_hour_fun > time_after), (start_hour_fun < time_before)], - ) + reduce( + operator.or_, + [(start_hour_fun > time_after), (start_hour_fun < time_before)], ) ) # all other cases should be and operator else: - event_filters.append((start_hour_fun > time_after)) - event_filters.append((start_hour_fun < time_before)) + event_filters.append(start_hour_fun > time_after) + event_filters.append(start_hour_fun < time_before) # Perform semantic search search_results = {} @@ -894,7 +889,7 @@ def events_search( @router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())]) def events_summary( params: EventsSummaryQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): tz_name = params.timezone has_clip = params.has_clip @@ -903,13 +898,13 @@ def events_summary( clauses = [] if has_clip is not None: - clauses.append((Event.has_clip == has_clip)) + clauses.append(Event.has_clip == has_clip) if has_snapshot is not None: - clauses.append((Event.has_snapshot == has_snapshot)) + clauses.append(Event.has_snapshot == has_snapshot) if len(clauses) == 0: - clauses.append((True)) + clauses.append(True) time_range_query = ( Event.select( diff --git a/frigate/api/export.py b/frigate/api/export.py index 09ded84124..741a0bb5dc 100644 --- a/frigate/api/export.py +++ b/frigate/api/export.py @@ -7,8 +7,8 @@ import string import time import zipfile from collections import deque +from collections.abc import Iterator from pathlib import Path -from typing import Iterator, List, Optional import psutil from fastapi import APIRouter, Depends, Query, Request @@ -68,6 +68,7 @@ from frigate.jobs.export import ( from frigate.models import Export, ExportCase, Previews, Recordings from frigate.record.export import ( DEFAULT_TIME_LAPSE_FFMPEG_ARGS, + ChaptersEnum, PlaybackSourceEnum, validate_ffmpeg_args, ) @@ -88,7 +89,7 @@ def _generate_export_id(camera_name: str) -> str: def _create_export_case_record( name: str, - description: Optional[str], + description: str | None, ) -> ExportCase: now = datetime.datetime.fromtimestamp(time.time()) return ExportCase.create( @@ -100,7 +101,7 @@ def _create_export_case_record( ) -def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONResponse]: +def _validate_camera_name(request: Request, camera_name: str) -> JSONResponse | None: if camera_name and request.app.frigate_config.cameras.get(camera_name): return None @@ -110,7 +111,7 @@ def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONRe ) -def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONResponse]: +def _validate_export_case(export_case_id: str | None) -> JSONResponse | None: if export_case_id is None: return None @@ -126,8 +127,17 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons def _sanitize_existing_image( - image_path: Optional[str], -) -> tuple[Optional[str], Optional[JSONResponse]]: + image_path: str | None, +) -> tuple[str | None, JSONResponse | None]: + # 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): @@ -144,7 +154,7 @@ def _validate_export_source( start_time: float, end_time: float, playback_source: PlaybackSourceEnum, -) -> Optional[str]: +) -> str | None: if playback_source == PlaybackSourceEnum.recordings: recordings_count = ( Recordings.select() @@ -247,13 +257,14 @@ def _build_export_job( camera_name: str, start_time: float, end_time: float, - friendly_name: Optional[str], - existing_image: Optional[str], + friendly_name: str | None, + existing_image: str | None, playback_source: PlaybackSourceEnum, - export_case_id: Optional[str], - ffmpeg_input_args: Optional[str] = None, - ffmpeg_output_args: Optional[str] = None, + export_case_id: str | None, + ffmpeg_input_args: str | None = None, + ffmpeg_output_args: str | None = None, cpu_fallback: bool = False, + chapters: ChaptersEnum | None = None, ) -> ExportJob: return ExportJob( id=_generate_export_id(camera_name), @@ -267,6 +278,7 @@ def _build_export_job( ffmpeg_input_args=ffmpeg_input_args, ffmpeg_output_args=ffmpeg_output_args, cpu_fallback=cpu_fallback, + chapters=chapters, ) @@ -290,11 +302,11 @@ def _export_case_to_dict(case: ExportCase) -> dict[str, object]: Returns a list of exports ordered by date (most recent first).""", ) def get_exports( - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), - export_case_id: Optional[str] = None, - cameras: Optional[str] = Query(default="all"), - start_date: Optional[float] = None, - end_date: Optional[float] = None, + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), + export_case_id: str | None = None, + cameras: str | None = Query(default="all"), + start_date: float | None = None, + end_date: float | None = None, ): query = Export.select().where(Export.camera << allowed_cameras) @@ -410,7 +422,7 @@ def _unique_archive_name(export: Export, used: set[str]) -> str: return candidate -def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]: +def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]: """Yield bytes of a zip archive built from the given exports' mp4 files.""" buffer = _StreamingZipBuffer() used_names: set[str] = set() @@ -454,7 +466,7 @@ def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]: ) def download_export_case( case_id: str, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): try: case = ExportCase.get(ExportCase.id == case_id) @@ -568,7 +580,7 @@ def delete_export_case(case_id: str, request: Request, delete_exports: bool = Fa ) def get_active_export_jobs( request: Request, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): jobs = list_active_export_jobs(request.app.frigate_config) return JSONResponse( @@ -610,7 +622,7 @@ async def get_export_job_status(export_id: str, request: Request): def export_recordings_batch( request: Request, body: BatchExportBody, - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), current_user: dict = Depends(get_current_user), ): if isinstance(current_user, JSONResponse): @@ -650,7 +662,7 @@ def export_recordings_batch( # Sanitize each item's image_path up front. A bad path in any item # kills the whole request, consistent with single-export behavior. - sanitized_images: list[Optional[str]] = [] + sanitized_images: list[str | None] = [] for item in body.items: existing_image, image_validation_error = _sanitize_existing_image( item.image_path @@ -701,7 +713,7 @@ def export_recordings_batch( export_case_id = export_case.id export_ids: list[str] = [] - results: list[dict[str, Optional[str] | bool | int]] = [] + results: list[dict[str, str | None | bool | int]] = [] for index, item in enumerate(body.items): if index in item_errors: results.append( @@ -725,6 +737,9 @@ def export_recordings_batch( sanitized_images[index], PlaybackSourceEnum.recordings, export_case_id, + chapters=request.app.frigate_config.cameras[ + item.camera + ].record.export.chapters, ) try: start_export_job(request.app.frigate_config, export_job) @@ -803,6 +818,14 @@ def export_recording( export_case_id = body.export_case_id + # a chapters value in the request body overrides the camera's export config + camera_config = request.app.frigate_config.cameras[camera_name] + chapters = ( + body.chapters + if body.chapters is not None + else camera_config.record.export.chapters + ) + # Attaching to an existing case requires admin. Single-export for # cameras the user can access is otherwise non-admin; we only gate # the case-attachment side effect. @@ -839,6 +862,7 @@ def export_recording( existing_image, playback_source, export_case_id, + chapters=chapters, ) try: start_export_job(request.app.frigate_config, export_job) diff --git a/frigate/api/fastapi_app.py b/frigate/api/fastapi_app.py index 3f8d8a7a5f..5e8323b58f 100644 --- a/frigate/api/fastapi_app.py +++ b/frigate/api/fastapi_app.py @@ -1,7 +1,6 @@ import asyncio import logging import re -from typing import Optional from fastapi import Depends, FastAPI, Request from fastapi.responses import JSONResponse @@ -64,7 +63,7 @@ class RemoteUserPlugin(Plugin): def create_fastapi_app( frigate_config: FrigateConfig, database: SqliteQueueDatabase, - embeddings: Optional[EmbeddingsContext], + embeddings: EmbeddingsContext | None, detected_frames_processor, storage_maintainer: StorageMaintainer, onvif: OnvifController, @@ -72,8 +71,8 @@ def create_fastapi_app( event_metadata_updater: EventMetadataPublisher, config_publisher: CameraConfigUpdatePublisher, replay_manager: DebugReplayManager, - dispatcher: Optional[Dispatcher] = None, - profile_manager: Optional[ProfileManager] = None, + dispatcher: Dispatcher | None = None, + profile_manager: ProfileManager | None = None, enforce_default_admin: bool = True, ): logger.info("Starting FastAPI app") diff --git a/frigate/api/media.py b/frigate/api/media.py index c8285eda16..8d56475c59 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -7,7 +7,7 @@ import math import os import subprocess as sp import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path as FilePath from typing import Any from urllib.parse import unquote @@ -60,6 +60,19 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.media]) +def _resolve_cache_age(max_cache_age: int) -> int: + """Return max_cache_age as an int. + + When a media handler is invoked directly by another handler instead of + through its route, FastAPI doesn't resolve the Query() default and + max_cache_age arrives as the Query object; fall back to its int default. + """ + if isinstance(max_cache_age, int): + return max_cache_age + + return max_cache_age.default + + @router.get("/{camera_name}", dependencies=[Depends(require_camera_access)]) async def mjpeg_feed( request: Request, @@ -301,10 +314,8 @@ async def get_snapshot_from_recording( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -322,10 +333,8 @@ async def get_snapshot_from_recording( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -385,10 +394,7 @@ async def submit_recording_snapshot_to_plus( Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) @@ -413,7 +419,9 @@ async def submit_recording_snapshot_to_plus( ) nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) - request.app.frigate_config.plus_api.upload_image(nd, camera_name) + await asyncio.to_thread( + request.app.frigate_config.plus_api.upload_image, nd, camera_name + ) return JSONResponse( content={ @@ -704,7 +712,7 @@ async def vod_hour( ): parts = year_month.split("-") start_date = ( - datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=timezone.utc) + datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) @@ -936,7 +944,7 @@ async def event_thumbnail( thumbnail_bytes, media_type=extension.get_mime_type(), headers={ - "Cache-Control": f"private, max-age={max_cache_age}" + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}" if event_complete else "no-store", }, @@ -1270,14 +1278,14 @@ async def event_preview(request: Request, event_id: str): end_ts = start_ts + ( min(event.end_time - event.start_time, 20) if event.end_time else 20 ) - return preview_gif(request, event.camera, start_ts, end_ts) + return await preview_gif(request, event.camera, start_ts, end_ts) @router.get( "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif", dependencies=[Depends(require_camera_access)], ) -def preview_gif( +async def preview_gif( request: Request, camera_name: str, start_ts: float, @@ -1340,7 +1348,8 @@ def preview_gif( "-", ] - process = sp.run( + process = await asyncio.to_thread( + sp.run, ffmpeg_cmd, capture_output=True, ) @@ -1419,7 +1428,8 @@ def preview_gif( "-", ] - process = sp.run( + process = await asyncio.to_thread( + sp.run, ffmpeg_cmd, input=str.encode("\n".join(selected_previews)), capture_output=True, @@ -1438,7 +1448,7 @@ def preview_gif( gif_bytes, media_type="image/gif", headers={ - "Cache-Control": f"private, max-age={max_cache_age}", + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}", "Content-Type": "image/gif", }, ) @@ -1448,7 +1458,7 @@ def preview_gif( "/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4", dependencies=[Depends(require_camera_access)], ) -def preview_mp4( +async def preview_mp4( request: Request, camera_name: str, start_ts: float, @@ -1528,7 +1538,8 @@ def preview_mp4( path, ] - process = sp.run( + process = await asyncio.to_thread( + sp.run, ffmpeg_cmd, capture_output=True, ) @@ -1604,7 +1615,8 @@ def preview_mp4( path, ] - process = sp.run( + process = await asyncio.to_thread( + sp.run, ffmpeg_cmd, input=str.encode("\n".join(selected_previews)), capture_output=True, @@ -1619,7 +1631,7 @@ def preview_mp4( headers = { "Content-Description": "File Transfer", - "Cache-Control": f"private, max-age={max_cache_age}", + "Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}", "Content-Type": "video/mp4", "Content-Length": str(os.path.getsize(path)), # nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers @@ -1657,9 +1669,9 @@ async def review_preview( ) if format == "gif": - return preview_gif(request, review.camera, start_ts, end_ts) + return await preview_gif(request, review.camera, start_ts, end_ts) else: - return preview_mp4(request, review.camera, start_ts, end_ts) + return await preview_mp4(request, review.camera, start_ts, end_ts) @router.get( diff --git a/frigate/api/media_auth.py b/frigate/api/media_auth.py index cc06eb75cb..0630e3604f 100644 --- a/frigate/api/media_auth.py +++ b/frigate/api/media_auth.py @@ -14,7 +14,6 @@ from __future__ import annotations import logging import os from enum import Enum -from typing import Optional from urllib.parse import unquote, urlparse from peewee import DoesNotExist @@ -42,7 +41,7 @@ class MediaAuthResolution(str, Enum): UNKNOWN = "unknown" -def extract_path(original_url: Optional[str]) -> Optional[str]: +def extract_path(original_url: str | None) -> str | None: """Return the decoded path component of nginx's `X-Original-URL` header. nginx forwards the *raw* request URI (with `..` segments intact) via @@ -72,8 +71,8 @@ def extract_path(original_url: Optional[str]) -> Optional[str]: def resolve_media_uri( - uri: str, frigate_config: Optional[FrigateConfig] = None -) -> tuple[MediaAuthResolution, Optional[str]]: + uri: str, frigate_config: FrigateConfig | None = None +) -> tuple[MediaAuthResolution, str | None]: """Classify a URI and return the owning camera if applicable. `frigate_config` is used to disambiguate clip/review filenames whose @@ -100,7 +99,7 @@ def resolve_media_uri( def _resolve_recording( parts: list[str], -) -> tuple[MediaAuthResolution, Optional[str]]: +) -> tuple[MediaAuthResolution, str | None]: # /recordings → neutral # /recordings/{date} → neutral # /recordings/{date}/{hour} → multi-camera listing @@ -113,8 +112,8 @@ def _resolve_recording( def _resolve_clip( - parts: list[str], frigate_config: Optional[FrigateConfig] -) -> tuple[MediaAuthResolution, Optional[str]]: + parts: list[str], frigate_config: FrigateConfig | None +) -> tuple[MediaAuthResolution, str | None]: # /clips → multi-camera listing # /clips/thumbs/{cam}/... → camera # /clips/previews/{cam}/... → camera @@ -159,8 +158,8 @@ def _resolve_clip( def _longest_prefix_camera( - stem: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + stem: str, frigate_config: FrigateConfig | None +) -> str | None: if frigate_config is None: return None for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True): @@ -170,8 +169,8 @@ def _longest_prefix_camera( def _camera_from_clip_filename( - filename: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + filename: str, frigate_config: FrigateConfig | None +) -> str | None: """Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against configured camera names. Longest-prefix wins so camera names containing hyphens (e.g. `front-door`) resolve correctly. @@ -182,8 +181,8 @@ def _camera_from_clip_filename( def _camera_from_thumb_filename( - filename: str, frigate_config: Optional[FrigateConfig] -) -> Optional[str]: + filename: str, frigate_config: FrigateConfig | None +) -> str | None: """Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`.""" if not filename.startswith("thumb-"): return None @@ -194,7 +193,7 @@ def _camera_from_thumb_filename( def _resolve_export( parts: list[str], -) -> tuple[MediaAuthResolution, Optional[str]]: +) -> tuple[MediaAuthResolution, str | None]: # /exports → multi-camera listing # /exports/{filename}.mp4 → camera (DB lookup by exact path) if len(parts) == 1: @@ -240,8 +239,8 @@ def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool: def deny_response_for_media_uri( - original_url: Optional[str], role: Optional[str], frigate_config: FrigateConfig -) -> Optional[int]: + original_url: str | None, role: str | None, frigate_config: FrigateConfig +) -> int | None: """Decide whether the current role should be blocked from `original_url`. Returns an HTTP status code (403) when access should be denied, or `None` diff --git a/frigate/api/motion_search.py b/frigate/api/motion_search.py index 378e3469b7..8a5f175457 100644 --- a/frigate/api/motion_search.py +++ b/frigate/api/motion_search.py @@ -1,7 +1,7 @@ """Motion search API for detecting changes within a region of interest.""" import logging -from typing import Any, List, Optional +from typing import Any from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse @@ -26,7 +26,7 @@ class MotionSearchRequest(BaseModel): start_time: float = Field(description="Start timestamp for the search range") end_time: float = Field(description="End timestamp for the search range") - polygon_points: List[List[float]] = Field( + polygon_points: list[list[float]] = Field( description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon" ) threshold: int = Field( @@ -87,12 +87,12 @@ class MotionSearchStatusResponse(BaseModel): success: bool message: str status: str # "queued", "running", "success", "failed", or "cancelled" - results: Optional[List[MotionSearchResult]] = None - total_frames_processed: Optional[int] = None - error_message: Optional[str] = None - metrics: Optional[MotionSearchMetricsResponse] = None - scanning_timestamp: Optional[float] = None - progress: Optional[float] = None + results: list[MotionSearchResult] | None = None + total_frames_processed: int | None = None + error_message: str | None = None + metrics: MotionSearchMetricsResponse | None = None + scanning_timestamp: float | None = None + progress: float | None = None @router.post( diff --git a/frigate/api/preview.py b/frigate/api/preview.py index a307b5abce..1047591434 100644 --- a/frigate/api/preview.py +++ b/frigate/api/preview.py @@ -1,8 +1,10 @@ """Preview apis.""" +import bisect import logging import os -from datetime import datetime, timedelta, timezone +import threading +from datetime import UTC, datetime, timedelta import pytz from fastapi import APIRouter, Depends, HTTPException @@ -123,7 +125,7 @@ def preview_hour( """Get all mp4 previews relevant for time period given the timezone""" parts = year_month.split("-") start_date = ( - datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc) + datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) - datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) @@ -133,6 +135,32 @@ 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, @@ -149,23 +177,15 @@ 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}" - camera_files = [ - entry.name - for entry in os.scandir(preview_dir) - if entry.name.startswith(file_start) + 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.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, diff --git a/frigate/api/record.py b/frigate/api/record.py index 04b81cf38a..5db257f482 100644 --- a/frigate/api/record.py +++ b/frigate/api/record.py @@ -5,7 +5,6 @@ import logging from datetime import datetime, timedelta from functools import reduce from pathlib import Path -from typing import List from urllib.parse import unquote from fastapi import APIRouter, Depends, Request @@ -63,7 +62,7 @@ def get_recordings_storage_usage(request: Request): def all_recordings_summary( request: Request, params: MediaRecordingsSummaryQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Returns true/false by day indicating if recordings exist""" @@ -263,18 +262,18 @@ async def recordings( async def no_recordings( request: Request, params: MediaRecordingsAvailabilityQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get time ranges with no recordings.""" cameras = params.cameras if cameras != "all": requested = set(unquote(cameras).split(",")) - filtered = requested.intersection(allowed_cameras) - if not filtered: - return JSONResponse(content=[]) - cameras = ",".join(filtered) + camera_list = list(requested.intersection(allowed_cameras)) else: - cameras = allowed_cameras + camera_list = list(allowed_cameras) + + if not camera_list: + return JSONResponse(content=[]) before = params.before or datetime.datetime.now().timestamp() after = ( @@ -283,12 +282,10 @@ async def no_recordings( ) scale = params.scale - clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)] - if cameras != "all": - camera_list = cameras.split(",") - clauses.append((Recordings.camera << camera_list)) - else: - camera_list = allowed_cameras + clauses = [ + (Recordings.end_time >= after) & (Recordings.start_time <= before), + (Recordings.camera << camera_list), + ] # Get recording start times data: list[Recordings] = ( @@ -367,7 +364,7 @@ async def delete_recordings( start: float = PathParam(..., description="Start timestamp (unix)"), end: float = PathParam(..., description="End timestamp (unix)"), params: RecordingsDeleteQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Delete recordings in the specified time range.""" if start >= end: diff --git a/frigate/api/review.py b/frigate/api/review.py index ccf0be9d12..08705e3f9e 100644 --- a/frigate/api/review.py +++ b/frigate/api/review.py @@ -4,7 +4,6 @@ import datetime import logging from functools import reduce from pathlib import Path -from typing import List import pandas as pd from fastapi import APIRouter, Request @@ -51,7 +50,7 @@ router = APIRouter(tags=[Tags.review]) async def review( params: ReviewQueryParams = Depends(), current_user: dict = Depends(get_current_user), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): if isinstance(current_user, JSONResponse): return current_user @@ -83,7 +82,7 @@ async def review( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -106,12 +105,12 @@ async def review( for zone in filtered_zones: zone_clauses.append( - (ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*') + ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*' ) clauses.append(reduce(operator.or_, zone_clauses)) if severity: - clauses.append((ReviewSegment.severity == severity)) + clauses.append(ReviewSegment.severity == severity) # Join with UserReviewStatus to get per-user review status review_query = ( @@ -204,7 +203,7 @@ async def review_ids(request: Request, ids: str): async def review_summary( params: ReviewSummaryQueryParams = Depends(), current_user: dict = Depends(get_current_user), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): if isinstance(current_user, JSONResponse): return current_user @@ -227,7 +226,7 @@ async def review_summary( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -328,7 +327,7 @@ async def review_summary( camera_list = list(filtered) else: camera_list = allowed_cameras - clauses.append((ReviewSegment.camera << camera_list)) + clauses.append(ReviewSegment.camera << camera_list) if labels != "all": # use matching so segments with multiple labels @@ -584,7 +583,7 @@ def delete_reviews(body: ReviewModifyMultipleBody): ) def motion_activity( params: ReviewActivityMotionQueryParams = Depends(), - allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter), + allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter), ): """Get motion and audio activity.""" cameras = params.cameras @@ -597,7 +596,7 @@ def motion_activity( scale = params.scale clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)] - clauses.append((Recordings.motion > 0)) + clauses.append(Recordings.motion > 0) if cameras != "all": requested = set(cameras.split(",")) @@ -608,7 +607,7 @@ def motion_activity( else: camera_list = list(allowed_cameras) - clauses.append((Recordings.camera << camera_list)) + clauses.append(Recordings.camera << camera_list) data: list[Recordings] = ( Recordings.select( diff --git a/frigate/app.py b/frigate/app.py index 785dc24470..b0e29eab2a 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -4,11 +4,11 @@ import multiprocessing as mp import os import secrets import shutil +from collections.abc import Callable from multiprocessing import Queue from multiprocessing.managers import DictProxy, SyncManager from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Callable, Optional import psutil import uvicorn @@ -95,7 +95,7 @@ class FrigateApp: self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent ) -> None: self.metrics_manager = manager - self.audio_process: Optional[mp.Process] = None + self.audio_process: mp.Process | None = None self.stop_event = stop_event self.detection_queue: Queue = mp.Queue() self.detectors: dict[str, ObjectDetectProcess] = {} @@ -120,8 +120,8 @@ class FrigateApp: ) self.ptz_metrics: dict[str, PTZMetrics] = {} self.processes: dict[str, int] = {} - self.embeddings: Optional[EmbeddingsContext] = None - self.profile_manager: Optional[ProfileManager] = None + self.embeddings: EmbeddingsContext | None = None + self.profile_manager: ProfileManager | None = None self.config = config def ensure_dirs(self) -> None: diff --git a/frigate/camera/activity_manager.py b/frigate/camera/activity_manager.py index 38425add9a..bd3474b1ab 100644 --- a/frigate/camera/activity_manager.py +++ b/frigate/camera/activity_manager.py @@ -6,13 +6,18 @@ import logging import random import string from collections import Counter -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from frigate.comms.event_metadata_updater import ( EventMetadataPublisher, EventMetadataTypeEnum, ) from frigate.config import CameraConfig, FrigateConfig +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) logger = logging.getLogger(__name__) @@ -29,6 +34,11 @@ class CameraActivityManager: self.zone_all_object_counts: dict[str, Counter] = {} self.zone_active_object_counts: dict[str, Counter] = {} self.all_zone_labels: dict[str, set[str]] = {} + self.config_subscriber = CameraConfigUpdateSubscriber( + config, + config.cameras, + [CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects], + ) for camera_config in config.cameras.values(): if not camera_config.enabled_in_config: @@ -56,7 +66,40 @@ class CameraActivityManager: else camera_config.objects.track ) + def __rebuild_zone_labels(self) -> None: + """Rebuild zone label tracking after a runtime zones/objects change.""" + new_zone_labels: dict[str, set[str]] = {} + + for camera_config in self.config.cameras.values(): + if not camera_config.enabled_in_config or camera_config.name is None: + continue + + for zone, zone_config in camera_config.zones.items(): + new_zone_labels.setdefault(zone, set()).update( + zone_config.objects + if zone_config.objects + else camera_config.objects.track + ) + + # drop counters for zones that no longer exist + for zone in list(self.zone_all_object_counts.keys()): + if zone not in new_zone_labels: + self.zone_all_object_counts.pop(zone, None) + self.zone_active_object_counts.pop(zone, None) + + # ensure counters exist for new zones so the first count is published + for zone in new_zone_labels: + self.zone_all_object_counts.setdefault(zone, Counter()) + self.zone_active_object_counts.setdefault(zone, Counter()) + + self.all_zone_labels = new_zone_labels + def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: + updated_topics = self.config_subscriber.check_for_updates() + + if "zones" in updated_topics or "objects" in updated_topics: + self.__rebuild_zone_labels() + all_objects: list[dict[str, Any]] = [] for camera in new_activity.keys(): @@ -161,6 +204,9 @@ class CameraActivityManager: self.publish(f"{camera}/all", sum(list(all_objects.values()))) self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) + def stop(self) -> None: + self.config_subscriber.stop() + class AudioActivityManager: def __init__( diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index ea8df7bff0..9f63ead2da 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -117,7 +117,9 @@ class CameraMaintainer(threading.Thread): if runtime: self.camera_metrics[name] = CameraMetrics(self.metrics_manager) - self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) + self.ptz_metrics[name] = PTZMetrics( + autotracker_enabled=config.onvif.autotracking.enabled + ) self.region_grids[name] = get_camera_regions_grid( name, config.detect, diff --git a/frigate/camera/state.py b/frigate/camera/state.py index f35a3eaa56..fed819154e 100644 --- a/frigate/camera/state.py +++ b/frigate/camera/state.py @@ -5,7 +5,8 @@ import logging import os import threading from collections import defaultdict -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import cv2 import numpy as np @@ -110,9 +111,9 @@ class CameraState: # draw thicker box around ptz autotracked object if ( self.camera_config.onvif.autotracking.enabled - and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init[ + and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init.get( self.name - ] + ) and self.ptz_autotracker_thread.ptz_autotracker.tracked_object[ self.name ] diff --git a/frigate/comms/base_communicator.py b/frigate/comms/base_communicator.py index 5dfbf1115e..090552a5bd 100644 --- a/frigate/comms/base_communicator.py +++ b/frigate/comms/base_communicator.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Any, Callable +from collections.abc import Callable +from typing import Any class Communicator(ABC): diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index a85e644940..6cb4f21b07 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -3,8 +3,8 @@ import datetime import json import logging -from collections.abc import Iterable -from typing import Any, Callable, Optional, cast +from collections.abc import Callable, Iterable +from typing import Any, cast from frigate.camera import PTZMetrics from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager @@ -97,7 +97,7 @@ class Dispatcher: "notifications": self._on_global_notification_command, "profile": self._on_profile_command, } - self.profile_manager: Optional[ProfileManager] = None + self.profile_manager: ProfileManager | None = None for comm in self.comms: comm.subscribe(self._receive) @@ -105,8 +105,10 @@ class Dispatcher: self.web_push_client = next( (comm for comm in communicators if isinstance(comm, WebPushClient)), None ) + if self.web_push_client is not None: + self.web_push_client.set_suspension_broadcaster(self.publish) - def _receive(self, topic: str, payload: Any) -> Optional[Any]: + def _receive(self, topic: str, payload: Any) -> Any | None: """Handle receiving of payload from communicators.""" def handle_camera_command( @@ -397,6 +399,8 @@ class Dispatcher: comm.publish(topic, payload, retain) def stop(self) -> None: + self.camera_activity.stop() + for comm in self.comms: comm.stop() @@ -584,6 +588,10 @@ class Dispatcher: self.ptz_metrics[camera_name].start_time.value = 0 ptz_autotracker_settings.enabled = False + self.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.autotracking, camera_name), + ptz_autotracker_settings, + ) self.publish(f"{camera_name}/ptz_autotracker/state", payload, retain=True) def _on_motion_contour_area_command(self, camera_name: str, payload: int) -> None: diff --git a/frigate/comms/embeddings_updater.py b/frigate/comms/embeddings_updater.py index f7fd9c2bf3..cd83709f0b 100644 --- a/frigate/comms/embeddings_updater.py +++ b/frigate/comms/embeddings_updater.py @@ -1,8 +1,9 @@ """Facilitates communication between processes.""" import logging +from collections.abc import Callable from enum import Enum -from typing import Any, Callable +from typing import Any import zmq diff --git a/frigate/comms/inter_process.py b/frigate/comms/inter_process.py index 5e76da5ebd..6897b12ebf 100644 --- a/frigate/comms/inter_process.py +++ b/frigate/comms/inter_process.py @@ -3,8 +3,9 @@ import logging import multiprocessing as mp import threading +from collections.abc import Callable from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Callable +from typing import Any import zmq diff --git a/frigate/comms/mqtt.py b/frigate/comms/mqtt.py index 89a986e08c..979fc6d6c0 100644 --- a/frigate/comms/mqtt.py +++ b/frigate/comms/mqtt.py @@ -1,6 +1,7 @@ import logging import threading -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import paho.mqtt.client as mqtt from paho.mqtt.enums import CallbackAPIVersion @@ -41,6 +42,18 @@ class MqttClient(Communicator): self.publish("available", "stopped", retain=True) self.client.disconnect() + def _notifications_enabled_in_config(self) -> bool: + """Whether notifications are configured globally or on any camera. + + Notifications can be enabled per camera with the global config left + disabled, so the global topics must consider both (matching how + app.py decides to create the WebPushClient). + """ + return self.config.notifications.enabled_in_config or any( + cam.enabled and cam.notifications.enabled_in_config + for cam in self.config.cameras.values() + ) + def _set_initial_topics(self) -> None: """Set initial state topics.""" for camera_name, camera in self.config.cameras.items(): @@ -157,7 +170,7 @@ class MqttClient(Communicator): retain=True, ) - if self.config.notifications.enabled_in_config: + if self._notifications_enabled_in_config(): self.publish( "notifications/state", "ON" if self.config.notifications.enabled else "OFF", @@ -202,8 +215,8 @@ class MqttClient(Communicator): logger.error("Unable to connect to MQTT server: MQTT Not authorized") else: logger.error( - "Unable to connect to MQTT server: Connection refused. Error code: " - + reason_code.getName() + "Unable to connect to MQTT server: Connection refused. Error code: %s", + reason_code.getName(), ) self.connected = True @@ -256,6 +269,7 @@ class MqttClient(Communicator): "review_detections", "object_descriptions", "review_descriptions", + "notifications", ] for name in self.config.cameras.keys(): @@ -265,6 +279,12 @@ class MqttClient(Communicator): self.on_mqtt_command, ) + # notifications suspend doesn't follow the /set topic pattern + self.client.message_callback_add( + f"{self.mqtt_config.topic_prefix}/{name}/notifications/suspend", + self.on_mqtt_command, + ) + if self.config.cameras[name].onvif.host: self.client.message_callback_add( f"{self.mqtt_config.topic_prefix}/{name}/ptz", @@ -289,7 +309,7 @@ class MqttClient(Communicator): self.on_mqtt_command, ) - if self.config.notifications.enabled_in_config: + if self._notifications_enabled_in_config(): self.client.message_callback_add( f"{self.mqtt_config.topic_prefix}/notifications/set", self.on_mqtt_command, diff --git a/frigate/comms/runtime_state.py b/frigate/comms/runtime_state.py index 5066ed3993..222d488ec9 100644 --- a/frigate/comms/runtime_state.py +++ b/frigate/comms/runtime_state.py @@ -146,7 +146,7 @@ class RuntimeStatePersistence: if not os.path.exists(self._path): return {} try: - with open(self._path, "r") as f: + with open(self._path) as f: data = json.load(f) except (OSError, json.JSONDecodeError): logger.exception( diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index e35b64762c..6f2d9ab8da 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -6,9 +6,10 @@ import logging import os import queue import threading +from collections.abc import Callable from dataclasses import dataclass from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Callable +from typing import Any from py_vapid import Vapid01 from pywebpush import WebPusher @@ -54,6 +55,7 @@ class WebPushClient(Communicator): c.name: 0 # type: ignore[misc] for c in self.config.cameras.values() } + self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None self.last_camera_notification_time: dict[str, float] = { c.name: 0 # type: ignore[misc] for c in self.config.cameras.values() @@ -65,6 +67,10 @@ class WebPushClient(Communicator): target=self._process_notifications, daemon=True ) self.notification_thread.start() + self.suspension_thread = threading.Thread( + target=self._process_suspensions, daemon=True + ) + self.suspension_thread.start() if not self.config.notifications.email: logger.warning("Email must be provided for push notifications to be sent.") @@ -163,6 +169,27 @@ class WebPushClient(Communicator): def is_camera_suspended(self, camera: str) -> bool: return datetime.datetime.now().timestamp() <= self.suspended_cameras[camera] + def set_suspension_broadcaster( + self, broadcaster: Callable[[str, Any, bool], None] + ) -> None: + """Register the callback used to broadcast suspension state changes.""" + self.suspension_broadcaster = broadcaster + + def _process_suspensions(self) -> None: + while not self.stop_event.wait(1): + self._clear_expired_suspensions() + + def _clear_expired_suspensions(self) -> None: + """Reset and broadcast cameras whose suspension window has elapsed.""" + now = datetime.datetime.now().timestamp() + for camera, suspended_until in list(self.suspended_cameras.items()): + if suspended_until and now > suspended_until: + self.unsuspend_notifications(camera) + if self.suspension_broadcaster is not None: + self.suspension_broadcaster( + f"{camera}/notifications/suspended", "0", True + ) + def publish(self, topic: str, payload: Any, retain: bool = False) -> None: """Wrapper for publishing when client is in valid state.""" # check for updated global config (notifications, auth) diff --git a/frigate/comms/ws.py b/frigate/comms/ws.py index 5b555999e3..ac047b053f 100644 --- a/frigate/comms/ws.py +++ b/frigate/comms/ws.py @@ -4,7 +4,8 @@ import errno import json import logging import threading -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import ( @@ -72,11 +73,16 @@ _WS_VIEWER_TOPICS = frozenset( } ) +# Camera-scoped command topics a camera-authorized (non-admin) user may send. +_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"}) + def _check_ws_authorization( topic: str, role_header: str | None, separator: str, + roles_config: dict[str, list[str]] | None = None, + camera_names: set[str] | None = None, ) -> bool: """Check if a WebSocket message is authorized. @@ -84,6 +90,10 @@ def _check_ws_authorization( topic: The message topic. role_header: The HTTP_REMOTE_ROLE header value, or None. separator: The role separator character from proxy config. + roles_config: The auth.roles mapping (role -> allowed cameras), used to + authorize camera-scoped commands for non-admin users. + camera_names: All configured camera names, used to resolve a role's + allowed cameras. Returns: True if authorized, False if blocked. @@ -93,16 +103,33 @@ def _check_ws_authorization( return False # No role header: default to viewer (fail-closed) - if role_header is None: - return topic in _WS_VIEWER_TOPICS + roles = [r.strip() for r in role_header.split(separator)] if role_header else [] - # Check if any role is admin - roles = [r.strip() for r in role_header.split(separator)] + # Admin can send anything if "admin" in roles: return True - # Non-admin: only viewer topics allowed - return topic in _WS_VIEWER_TOPICS + # Read-only topics any authenticated user can send + if topic in _WS_VIEWER_TOPICS: + return True + + # Camera-scoped command like "/ptz": allow when the user's role(s) + # grant access to that camera. + parts = topic.split("/") + if ( + roles_config is not None + and len(parts) == 2 + and parts[1] in _WS_CAMERA_COMMAND_TOPICS + ): + allowed: set[str] = set() + # No role header maps to the default viewer role (e.g. proxy-only setups) + for role in roles or ["viewer"]: + allowed.update( + User.get_allowed_cameras(role, roles_config, camera_names or set()) + ) + return parts[0] in allowed + + return False # ---- Outbound filtering --------------------------------------------------- @@ -449,6 +476,8 @@ class WebSocketClient(Communicator): class _WebSocketHandler(WebSocket): receiver = self._dispatcher role_separator = self.config.proxy.separator or "," + roles_config = self.config.auth.roles + camera_names = set(self.config.cameras.keys()) def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined] try: @@ -470,7 +499,11 @@ class WebSocketClient(Communicator): self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None ) if self.environ is not None and not _check_ws_authorization( - topic, role_header, self.role_separator + topic, + role_header, + self.role_separator, + self.roles_config, + self.camera_names, ): logger.warning( "Blocked unauthorized WebSocket message: topic=%s, role=%s", diff --git a/frigate/config/auth.py b/frigate/config/auth.py index fccbfbaf2c..04beeb7757 100644 --- a/frigate/config/auth.py +++ b/frigate/config/auth.py @@ -1,5 +1,3 @@ -from typing import Dict, List, Optional - from pydantic import Field, field_validator, model_validator from .base import FrigateBaseModel @@ -41,7 +39,7 @@ class AuthConfig(FrigateBaseModel): description="When a session is within this many seconds of expiring, refresh it back to full length.", ge=30, ) - failed_login_rate_limit: Optional[str] = Field( + failed_login_rate_limit: str | None = Field( default=None, title="Failed login limits", description="Rate limiting rules for failed login attempts to reduce brute-force attacks.", @@ -57,12 +55,12 @@ class AuthConfig(FrigateBaseModel): title="Hash iterations", description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", ) - roles: Dict[str, List[str]] = Field( + roles: dict[str, list[str]] = Field( default_factory=dict, title="Role mappings", description="Map roles to camera lists. An empty list grants access to all cameras for the role.", ) - admin_first_time_login: Optional[bool] = Field( + admin_first_time_login: bool | None = Field( default=False, title="First-time admin flag", description=( @@ -72,7 +70,7 @@ class AuthConfig(FrigateBaseModel): @field_validator("roles") @classmethod - def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]: + def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]: # Ensure role names are valid (alphanumeric with underscores) for role in v.keys(): if not role.replace("_", "").isalnum(): diff --git a/frigate/config/camera/audio.py b/frigate/config/camera/audio.py index 6028802df9..813c2988db 100644 --- a/frigate/config/camera/audio.py +++ b/frigate/config/camera/audio.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from frigate.const import AUDIO_MIN_CONFIDENCE @@ -9,7 +7,7 @@ from ..base import FrigateBaseModel __all__ = ["AudioConfig", "AudioFilterConfig"] -DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"] +DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "speech", "yell"] class AudioFilterConfig(FrigateBaseModel): @@ -41,14 +39,14 @@ class AudioConfig(FrigateBaseModel): listen: list[str] = Field( default=DEFAULT_LISTEN_AUDIO, title="Listen types", - description="List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell).", + description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", ) - filters: Optional[dict[str, AudioFilterConfig]] = Field( + filters: dict[str, AudioFilterConfig] | None = Field( None, title="Audio filters", description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( None, title="Original audio state", description="Indicates whether audio detection was originally enabled in the static config file.", diff --git a/frigate/config/camera/birdseye.py b/frigate/config/camera/birdseye.py index 32aa66a985..b51c73d3c6 100644 --- a/frigate/config/camera/birdseye.py +++ b/frigate/config/camera/birdseye.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import BaseModel, Field @@ -35,7 +34,7 @@ class BirdseyeLayoutConfig(FrigateBaseModel): ge=1.0, le=5.0, ) - max_cameras: Optional[int] = Field( + max_cameras: int | None = Field( default=None, title="Max cameras", description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", diff --git a/frigate/config/camera/camera.py b/frigate/config/camera/camera.py index 01092d4f18..b9d2cef727 100644 --- a/frigate/config/camera/camera.py +++ b/frigate/config/camera/camera.py @@ -1,6 +1,5 @@ import os from enum import Enum -from typing import Optional from pydantic import Field, PrivateAttr, model_validator @@ -51,14 +50,14 @@ class CameraTypeEnum(str, Enum): class CameraConfig(FrigateBaseModel): - name: Optional[str] = Field( + name: str | None = Field( None, title="Camera name", description="Camera name is required", pattern=REGEX_CAMERA_NAME, ) - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Friendly name", description="Camera friendly name used in the Frigate UI", @@ -100,8 +99,8 @@ class CameraConfig(FrigateBaseModel): description="Settings for face detection and recognition for this camera.", ) ffmpeg: CameraFfmpegConfig = Field( - title="FFmpeg", - description="FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + title="Streams (FFmpeg)", + description="Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", ) live: CameraLiveConfig = Field( default_factory=CameraLiveConfig, @@ -180,7 +179,7 @@ class CameraConfig(FrigateBaseModel): title="Camera UI", description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", ) - webui_url: Optional[str] = Field( + webui_url: str | None = Field( None, title="Camera URL", description="URL to visit the camera directly from system page", @@ -196,7 +195,7 @@ class CameraConfig(FrigateBaseModel): title="Zones", description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original camera state", description="Keep track of original state of camera.", diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py index 71dbc32928..d093ed986e 100644 --- a/frigate/config/camera/detect.py +++ b/frigate/config/camera/detect.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field, model_validator from ..base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"] class StationaryMaxFramesConfig(FrigateBaseModel): - default: Optional[int] = Field( + default: int | None = Field( default=None, title="Default max frames", description="Default maximum frames to track a stationary object before stopping.", @@ -22,13 +20,13 @@ class StationaryMaxFramesConfig(FrigateBaseModel): class StationaryConfig(FrigateBaseModel): - interval: Optional[int] = Field( + interval: int | None = Field( default=None, title="Stationary interval", description="How often (in frames) to run a detection check to confirm a stationary object.", gt=0, ) - threshold: Optional[int] = Field( + threshold: int | None = Field( default=None, title="Stationary threshold", description="Number of frames with no position change required to mark an object as stationary.", @@ -52,12 +50,12 @@ class DetectConfig(FrigateBaseModel): title="Enable object detection", description="Enable or disable object detection for all cameras; can be overridden per-camera.", ) - height: Optional[int] = Field( + height: int | None = Field( default=None, title="Detect height", description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", ) - width: Optional[int] = Field( + width: int | None = Field( default=None, title="Detect width", description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", @@ -67,13 +65,13 @@ class DetectConfig(FrigateBaseModel): title="Detect FPS", description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", ) - min_initialized: Optional[int] = Field( + min_initialized: int | None = Field( default=None, title="Minimum initialization frames", description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", ge=2, ) - max_disappeared: Optional[int] = Field( + max_disappeared: int | None = Field( default=None, title="Maximum disappeared frames", description="Number of frames without a detection before a tracked object is considered gone.", diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 6341cbcd13..ad7cbc8aa1 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Union from pydantic import Field, field_validator @@ -33,12 +32,12 @@ DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [ class FfmpegOutputArgsConfig(FrigateBaseModel): - detect: Union[str, list[str]] = Field( + detect: str | list[str] = Field( default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT, title="Detect output arguments", description="Default output arguments for detect role streams.", ) - record: Union[str, list[str]] = Field( + record: str | list[str] = Field( default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT, title="Record output arguments", description="Default output arguments for record role streams.", @@ -49,19 +48,19 @@ class FfmpegConfig(FrigateBaseModel): path: str = Field( default="default", title="FFmpeg path", - description='Path to the FFmpeg binary to use or a version alias ("5.0" or "8.0").', + description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").', ) - global_args: Union[str, list[str]] = Field( + global_args: str | list[str] = Field( default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="FFmpeg global arguments", description="Global arguments passed to FFmpeg processes.", ) - hwaccel_args: Union[str, list[str]] = Field( + hwaccel_args: str | list[str] = Field( default="auto", title="Hardware acceleration arguments", description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", ) - input_args: Union[str, list[str]] = Field( + input_args: str | list[str] = Field( default=FFMPEG_INPUT_ARGS_DEFAULT, title="Input arguments", description="Input arguments applied to FFmpeg input streams.", @@ -112,17 +111,17 @@ class CameraInput(FrigateBaseModel): title="Input roles", description="Roles for this input stream.", ) - global_args: Union[str, list[str]] = Field( + global_args: str | list[str] = Field( default_factory=list, title="FFmpeg global arguments", description="FFmpeg global arguments for this input stream.", ) - hwaccel_args: Union[str, list[str]] = Field( + hwaccel_args: str | list[str] = Field( default_factory=list, title="Hardware acceleration arguments", description="Hardware acceleration arguments for this input stream.", ) - input_args: Union[str, list[str]] = Field( + input_args: str | list[str] = Field( default_factory=list, title="Input arguments", description="Input arguments specific to this stream.", diff --git a/frigate/config/camera/genai.py b/frigate/config/camera/genai.py index 5b94755723..82358252f8 100644 --- a/frigate/config/camera/genai.py +++ b/frigate/config/camera/genai.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Optional +from typing import Any from pydantic import Field @@ -26,12 +26,12 @@ class GenAIRoleEnum(str, Enum): class GenAIConfig(FrigateBaseModel): """Primary GenAI Config to define GenAI Provider.""" - api_key: Optional[EnvString] = Field( + api_key: EnvString | None = Field( default=None, title="API key", description="API key required by some providers (can also be set via environment variables).", ) - base_url: Optional[str] = Field( + base_url: str | None = Field( default=None, title="Base URL", description="Base URL for self-hosted or compatible providers (for example an Ollama instance).", diff --git a/frigate/config/camera/live.py b/frigate/config/camera/live.py index 54b5a2bfd5..90a7c8e4c1 100644 --- a/frigate/config/camera/live.py +++ b/frigate/config/camera/live.py @@ -1,5 +1,3 @@ -from typing import Dict - from pydantic import Field from ..base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["CameraLiveConfig"] class CameraLiveConfig(FrigateBaseModel): - streams: Dict[str, str] = Field( + streams: dict[str, str] = Field( default_factory=list, title="Live stream names", description="Mapping of configured stream names to restream/go2rtc names used for live playback.", diff --git a/frigate/config/camera/mask.py b/frigate/config/camera/mask.py index dbe0f063ce..ecfdb418b4 100644 --- a/frigate/config/camera/mask.py +++ b/frigate/config/camera/mask.py @@ -1,6 +1,6 @@ """Mask configuration for motion and object masks.""" -from typing import Any, Optional, Union +from typing import Any from pydantic import Field, field_serializer @@ -12,7 +12,7 @@ __all__ = ["MotionMaskConfig", "ObjectMaskConfig"] class MotionMaskConfig(FrigateBaseModel): """Configuration for a single motion mask.""" - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", description="A friendly name for this motion mask used in the Frigate UI", @@ -22,13 +22,13 @@ class MotionMaskConfig(FrigateBaseModel): title="Enabled", description="Enable or disable this motion mask", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( default="", title="Coordinates", description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", ) - raw_coordinates: Union[str, list[str]] = "" - enabled_in_config: Optional[bool] = Field( + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of motion mask." ) @@ -50,7 +50,7 @@ class MotionMaskConfig(FrigateBaseModel): class ObjectMaskConfig(FrigateBaseModel): """Configuration for a single object mask.""" - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( default=None, title="Friendly name", description="A friendly name for this object mask used in the Frigate UI", @@ -60,13 +60,13 @@ class ObjectMaskConfig(FrigateBaseModel): title="Enabled", description="Enable or disable this object mask", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( default="", title="Coordinates", description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", ) - raw_coordinates: Union[str, list[str]] = "" - enabled_in_config: Optional[bool] = Field( + raw_coordinates: str | list[str] = "" + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of object mask." ) diff --git a/frigate/config/camera/motion.py b/frigate/config/camera/motion.py index ebba8613cb..c44c1453f6 100644 --- a/frigate/config/camera/motion.py +++ b/frigate/config/camera/motion.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import Field, field_serializer @@ -28,7 +28,7 @@ class MotionConfig(FrigateBaseModel): ge=0.3, le=1.0, ) - skip_motion_threshold: Optional[float] = Field( + skip_motion_threshold: float | None = Field( default=None, title="Skip motion threshold", description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", @@ -40,7 +40,7 @@ class MotionConfig(FrigateBaseModel): title="Improve contrast", description="Apply contrast improvement to frames before motion analysis to help detection.", ) - contour_area: Optional[int] = Field( + contour_area: int | None = Field( default=10, title="Contour area", description="Minimum contour area in pixels required for a motion contour to be counted.", @@ -55,12 +55,12 @@ class MotionConfig(FrigateBaseModel): title="Frame alpha", description="Alpha value used when blending frames for motion preprocessing.", ) - frame_height: Optional[int] = Field( + frame_height: int | None = Field( default=100, title="Frame height", description="Height in pixels to scale frames to when computing motion.", ) - mask: dict[str, Optional[MotionMaskConfig]] = Field( + mask: dict[str, MotionMaskConfig | None] = Field( default_factory=dict, title="Mask coordinates", description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", @@ -70,12 +70,12 @@ class MotionConfig(FrigateBaseModel): title="MQTT off delay", description="Seconds to wait after last motion before publishing an MQTT 'off' state.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original motion state", description="Indicates whether motion detection was enabled in the original static configuration.", ) - raw_mask: dict[str, Optional[MotionMaskConfig]] = Field( + raw_mask: dict[str, MotionMaskConfig | None] = Field( default_factory=dict, exclude=True ) diff --git a/frigate/config/camera/notification.py b/frigate/config/camera/notification.py index dabf94675c..7f5968193f 100644 --- a/frigate/config/camera/notification.py +++ b/frigate/config/camera/notification.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from ..base import FrigateBaseModel @@ -13,7 +11,7 @@ class NotificationConfig(FrigateBaseModel): title="Enable notifications", description="Enable or disable notifications for all cameras; can be overridden per-camera.", ) - email: Optional[str] = Field( + email: str | None = Field( default=None, title="Notification email", description="Email address used for push notifications or required by certain notification providers.", @@ -24,7 +22,7 @@ class NotificationConfig(FrigateBaseModel): title="Cooldown period", description="Cooldown (seconds) between notifications to avoid spamming recipients.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original notifications state", description="Indicates whether notifications were enabled in the original static configuration.", diff --git a/frigate/config/camera/objects.py b/frigate/config/camera/objects.py index e93778f236..6b6759edf1 100644 --- a/frigate/config/camera/objects.py +++ b/frigate/config/camera/objects.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic import Field, PrivateAttr, field_serializer, field_validator @@ -12,12 +12,12 @@ DEFAULT_TRACKED_OBJECTS = ["person"] class FilterConfig(FrigateBaseModel): - min_area: Union[int, float] = Field( + min_area: int | float = Field( default=0, title="Minimum object area", description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", ) - max_area: Union[int, float] = Field( + max_area: int | float = Field( default=24000000, title="Maximum object area", description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", @@ -42,12 +42,12 @@ class FilterConfig(FrigateBaseModel): title="Minimum confidence", description="Minimum single-frame detection confidence required for the object to be counted.", ) - mask: dict[str, Optional[ObjectMaskConfig]] = Field( + mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, title="Filter mask", description="Polygon coordinates defining where this filter applies within the frame.", ) - raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field( + raw_mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, exclude=True ) @@ -68,7 +68,7 @@ class GenAIObjectTriggerConfig(FrigateBaseModel): title="Send on end", description="Send a request to GenAI when the tracked object ends.", ) - after_significant_updates: Optional[int] = Field( + after_significant_updates: int | None = Field( default=None, title="Early GenAI trigger", description="Send a request to GenAI after a specified number of significant updates for the tracked object.", @@ -98,12 +98,12 @@ class GenAIObjectConfig(FrigateBaseModel): description="Per-object prompts to customize GenAI outputs for specific labels.", ) - objects: Union[str, list[str]] = Field( + objects: str | list[str] = Field( default_factory=list, title="GenAI objects", description="List of object labels to send to GenAI by default.", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that must be entered for objects to qualify for GenAI description generation.", @@ -118,7 +118,7 @@ class GenAIObjectConfig(FrigateBaseModel): title="GenAI triggers", description="Defines when frames should be sent to GenAI (on end, after updates, etc.).", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original GenAI state", description="Indicates whether GenAI was enabled in the original static config.", @@ -144,12 +144,12 @@ class ObjectConfig(FrigateBaseModel): title="Object filters", description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).", ) - mask: dict[str, Optional[ObjectMaskConfig]] = Field( + mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, title="Object mask", description="Mask polygon used to prevent object detection in specified areas.", ) - raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field( + raw_mask: dict[str, ObjectMaskConfig | None] = Field( default_factory=dict, exclude=True ) genai: GenAIObjectConfig = Field( diff --git a/frigate/config/camera/onvif.py b/frigate/config/camera/onvif.py index 836dec6aae..01c9432819 100644 --- a/frigate/config/camera/onvif.py +++ b/frigate/config/camera/onvif.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field, field_validator @@ -59,12 +58,12 @@ class PtzAutotrackConfig(FrigateBaseModel): title="Return timeout", description="Wait this many seconds after losing tracking before returning camera to preset position.", ) - movement_weights: Optional[Union[str, list[str]]] = Field( + movement_weights: str | list[str] | None = Field( default_factory=list, title="Movement weights", description="Calibration values automatically generated by camera calibration. Do not modify manually.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original autotrack state", description="Internal field to track whether autotracking was enabled in configuration.", @@ -102,12 +101,12 @@ class OnvifConfig(FrigateBaseModel): title="ONVIF port", description="Port number for the ONVIF service.", ) - user: Optional[EnvString] = Field( + user: EnvString | None = Field( default=None, title="ONVIF username", description="Username for ONVIF authentication; some devices require admin user for ONVIF.", ) - password: Optional[EnvString] = Field( + password: EnvString | None = Field( default=None, title="ONVIF password", description="Password for ONVIF authentication.", @@ -117,7 +116,7 @@ class OnvifConfig(FrigateBaseModel): title="Disable TLS verify", description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", ) - profile: Optional[str] = Field( + profile: str | None = Field( default=None, title="ONVIF profile", description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", diff --git a/frigate/config/camera/profile.py b/frigate/config/camera/profile.py index 6a52a9ad6a..e3014aa4e6 100644 --- a/frigate/config/camera/profile.py +++ b/frigate/config/camera/profile.py @@ -1,7 +1,5 @@ """Camera profile configuration for named config overrides.""" -from typing import Optional - from ..base import FrigateBaseModel from ..classification import ( CameraFaceRecognitionConfig, @@ -29,16 +27,16 @@ class CameraProfileConfig(FrigateBaseModel): explicitly-set fields are used as overrides via exclude_unset. """ - enabled: Optional[bool] = None - audio: Optional[AudioConfig] = None - birdseye: Optional[BirdseyeCameraConfig] = None - detect: Optional[DetectConfig] = None - face_recognition: Optional[CameraFaceRecognitionConfig] = None - lpr: Optional[CameraLicensePlateRecognitionConfig] = None - motion: Optional[MotionConfig] = None - notifications: Optional[NotificationConfig] = None - objects: Optional[ObjectConfig] = None - record: Optional[RecordConfig] = None - review: Optional[ReviewConfig] = None - snapshots: Optional[SnapshotsConfig] = None - zones: Optional[dict[str, ZoneConfig]] = None + enabled: bool | None = None + audio: AudioConfig | None = None + birdseye: BirdseyeCameraConfig | None = None + detect: DetectConfig | None = None + face_recognition: CameraFaceRecognitionConfig | None = None + lpr: CameraLicensePlateRecognitionConfig | None = None + motion: MotionConfig | None = None + notifications: NotificationConfig | None = None + objects: ObjectConfig | None = None + record: RecordConfig | None = None + review: ReviewConfig | None = None + snapshots: SnapshotsConfig | None = None + zones: dict[str, ZoneConfig] | None = None diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py index 1f7afc6ceb..44a71c9cb9 100644 --- a/frigate/config/camera/record.py +++ b/frigate/config/camera/record.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field @@ -9,6 +8,7 @@ from frigate.review.types import SeverityEnum from ..base import FrigateBaseModel __all__ = [ + "ChaptersEnum", "RecordConfig", "RecordExportConfig", "RecordPreviewConfig", @@ -86,8 +86,14 @@ 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( + hwaccel_args: str | list[str] = Field( default="auto", title="Export hwaccel args", description="Hardware acceleration args to use for export/transcode operations.", @@ -98,6 +104,10 @@ class RecordExportConfig(FrigateBaseModel): title="Maximum concurrent exports", description="Maximum number of export jobs to process at the same time.", ) + chapters: ChaptersEnum = Field( + default=ChaptersEnum.review_items, + title="Chapter metadata to embed in exported recordings", + ) class RecordConfig(FrigateBaseModel): @@ -141,7 +151,7 @@ class RecordConfig(FrigateBaseModel): title="Preview config", description="Settings controlling the quality of recording previews shown in the UI.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original recording state", description="Indicates whether recording was enabled in the original static configuration.", diff --git a/frigate/config/camera/review.py b/frigate/config/camera/review.py index fbe24c98c4..f267f46ea2 100644 --- a/frigate/config/camera/review.py +++ b/frigate/config/camera/review.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional, Union from pydantic import Field, field_validator @@ -32,13 +31,13 @@ class AlertsConfig(FrigateBaseModel): title="Alert labels", description="List of object labels that qualify as alerts (for example: car, person).", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original alerts state", description="Tracks whether alerts were originally enabled in the static configuration.", @@ -67,12 +66,12 @@ class DetectionsConfig(FrigateBaseModel): description="Enable or disable detection events for all cameras; can be overridden per-camera.", ) - labels: Optional[list[str]] = Field( + labels: list[str] | None = Field( default=None, title="Detection labels", description="List of object labels that qualify as detection events.", ) - required_zones: Union[str, list[str]] = Field( + required_zones: str | list[str] = Field( default_factory=list, title="Required zones", description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.", @@ -83,7 +82,7 @@ class DetectionsConfig(FrigateBaseModel): description="Seconds to wait after no detection-causing activity before cutting off a detection.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original detections state", description="Tracks whether detections were originally enabled in the static configuration.", @@ -129,7 +128,7 @@ class GenAIReviewConfig(FrigateBaseModel): title="Save thumbnails", description="Save thumbnails that are sent to the GenAI provider for debugging and review.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original GenAI state", description="Tracks whether GenAI review was originally enabled in the static configuration.", diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py index 63bcba2267..7f5590a06c 100644 --- a/frigate/config/camera/snapshots.py +++ b/frigate/config/camera/snapshots.py @@ -1,9 +1,6 @@ -from typing import Optional - from pydantic import Field from ..base import FrigateBaseModel -from .record import RetainModeEnum __all__ = ["SnapshotsConfig", "RetainConfig"] @@ -14,11 +11,6 @@ class RetainConfig(FrigateBaseModel): title="Default retention", description="Default number of days to retain snapshots.", ) - mode: RetainModeEnum = Field( - default=RetainModeEnum.motion, - title="Retention mode", - description="Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", - ) objects: dict[str, float] = Field( default_factory=dict, title="Object retention", @@ -52,7 +44,7 @@ class SnapshotsConfig(FrigateBaseModel): title="Required zones", description="Zones an object must enter for a snapshot to be saved.", ) - height: Optional[int] = Field( + height: int | None = Field( default=None, title="Snapshot height", description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", diff --git a/frigate/config/camera/timestamp.py b/frigate/config/camera/timestamp.py index 48ec8240bd..1245c311bf 100644 --- a/frigate/config/camera/timestamp.py +++ b/frigate/config/camera/timestamp.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import Field @@ -76,7 +75,7 @@ class TimestampStyleConfig(FrigateBaseModel): title="Timestamp thickness", description="Line thickness of the timestamp text.", ) - effect: Optional[TimestampEffectEnum] = Field( + effect: TimestampEffectEnum | None = Field( default=None, title="Timestamp effect", description="Visual effect for the timestamp text (none, solid, shadow).", diff --git a/frigate/config/camera/updater.py b/frigate/config/camera/updater.py index b475f42157..c0b9260873 100644 --- a/frigate/config/camera/updater.py +++ b/frigate/config/camera/updater.py @@ -14,6 +14,7 @@ class CameraConfigUpdateEnum(str, Enum): add = "add" # for adding a camera audio = "audio" audio_transcription = "audio_transcription" + autotracking = "autotracking" # ptz autotracking only, without an onvif reinit birdseye = "birdseye" detect = "detect" enabled = "enabled" @@ -73,7 +74,12 @@ class CameraConfigUpdateSubscriber: base_topic = "config/cameras" - if len(self.camera_configs) == 1: + # global subscribers must hear every camera; only narrow per-camera workers + is_global_subscriber = ( + CameraConfigUpdateEnum.add in self.topics + or CameraConfigUpdateEnum.remove in self.topics + ) + if not is_global_subscriber and len(self.camera_configs) == 1: base_topic += f"/{list(self.camera_configs.keys())[0]}" self.subscriber = ConfigSubscriber( @@ -140,6 +146,8 @@ class CameraConfigUpdateSubscriber: config.snapshots = updated_config elif update_type == CameraConfigUpdateEnum.onvif: config.onvif = updated_config + elif update_type == CameraConfigUpdateEnum.autotracking: + config.onvif.autotracking = updated_config elif update_type == CameraConfigUpdateEnum.timestamp_style: config.timestamp_style = updated_config elif update_type == CameraConfigUpdateEnum.zones: diff --git a/frigate/config/camera/zone.py b/frigate/config/camera/zone.py index e4737f8dca..24c62e0ff6 100644 --- a/frigate/config/camera/zone.py +++ b/frigate/config/camera/zone.py @@ -1,6 +1,5 @@ # this uses the base model because the color is an extra attribute import logging -from typing import Optional, Union import numpy as np from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator @@ -13,7 +12,7 @@ logger = logging.getLogger(__name__) class ZoneConfig(BaseModel): - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Zone name", description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", @@ -23,7 +22,7 @@ class ZoneConfig(BaseModel): title="Enabled", description="Enable or disable this zone. Disabled zones are ignored at runtime.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Keep track of original state of zone." ) filters: dict[str, FilterConfig] = Field( @@ -31,11 +30,11 @@ class ZoneConfig(BaseModel): title="Zone filters", description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", ) - coordinates: Union[str, list[str]] = Field( + coordinates: str | list[str] = Field( title="Coordinates", description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", ) - distances: Optional[Union[str, list[str]]] = Field( + distances: str | list[str] | None = Field( default_factory=list, title="Real-world distances", description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", @@ -52,18 +51,18 @@ class ZoneConfig(BaseModel): title="Loitering seconds", description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", ) - speed_threshold: Optional[float] = Field( + speed_threshold: float | None = Field( default=None, ge=0.1, title="Minimum speed", description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", ) - objects: Union[str, list[str]] = Field( + objects: str | list[str] = Field( default_factory=list, title="Trigger objects", description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", ) - _color: Optional[tuple[int, int, int]] = PrivateAttr() + _color: tuple[int, int, int] | None = PrivateAttr() _contour: np.ndarray = PrivateAttr() @property @@ -147,7 +146,7 @@ class ZoneConfig(BaseModel): except ValueError: raise ValueError( f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" - ) + ) from None if explicit: self.coordinates = ",".join( @@ -176,7 +175,7 @@ class ZoneConfig(BaseModel): except ValueError: raise ValueError( f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}" - ) + ) from None if explicit: self.coordinates = ",".join( diff --git a/frigate/config/camera_group.py b/frigate/config/camera_group.py index 65319001ac..8e08f560fd 100644 --- a/frigate/config/camera_group.py +++ b/frigate/config/camera_group.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field, field_validator from .base import FrigateBaseModel @@ -8,7 +6,7 @@ __all__ = ["CameraGroupConfig"] class CameraGroupConfig(FrigateBaseModel): - cameras: Union[str, list[str]] = Field( + cameras: str | list[str] = Field( default_factory=list, title="Camera list", description="Array of camera names included in this group.", diff --git a/frigate/config/classification.py b/frigate/config/classification.py index 708f854e3a..f4dc5f6001 100644 --- a/frigate/config/classification.py +++ b/frigate/config/classification.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Dict, List, Optional, Union from pydantic import ConfigDict, Field, field_validator @@ -68,7 +67,7 @@ class AudioTranscriptionConfig(FrigateBaseModel): title="Model size", description="Model size to use for offline audio event transcription.", ) - live_enabled: Optional[bool] = Field( + live_enabled: bool | None = Field( default=False, title="Live transcription", description="Enable streaming live transcription for audio as it is received.", @@ -98,7 +97,7 @@ class CustomClassificationStateCameraConfig(FrigateBaseModel): class CustomClassificationStateConfig(FrigateBaseModel): - cameras: Dict[str, CustomClassificationStateCameraConfig] = Field( + cameras: dict[str, CustomClassificationStateCameraConfig] = Field( title="Classification cameras", description="Per-camera crop and settings for running state classification.", ) @@ -160,7 +159,7 @@ class ClassificationConfig(FrigateBaseModel): title="Bird classification config", description="Settings specific to bird classification models.", ) - custom: Dict[str, CustomClassificationConfig] = Field( + custom: dict[str, CustomClassificationConfig] = Field( default={}, title="Custom Classification Models", description="Configuration for custom classification models used for objects or state detection.", @@ -173,12 +172,12 @@ class SemanticSearchConfig(FrigateBaseModel): title="Enable semantic search", description="Enable or disable the semantic search feature.", ) - reindex: Optional[bool] = Field( + reindex: bool | None = Field( default=False, title="Reindex on startup", description="Trigger a full reindex of historical tracked objects into the embeddings database.", ) - model: Optional[Union[SemanticSearchModelEnum, str]] = Field( + model: SemanticSearchModelEnum | str | None = Field( default=SemanticSearchModelEnum.jinav1, title="Semantic search model or GenAI provider name", description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", @@ -199,7 +198,7 @@ class SemanticSearchConfig(FrigateBaseModel): title="Model size", description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", @@ -207,7 +206,7 @@ class SemanticSearchConfig(FrigateBaseModel): class TriggerConfig(FrigateBaseModel): - friendly_name: Optional[str] = Field( + friendly_name: str | None = Field( None, title="Friendly name", description="Optional friendly name displayed in the UI for this trigger.", @@ -233,7 +232,7 @@ class TriggerConfig(FrigateBaseModel): gt=0.0, le=1.0, ) - actions: List[TriggerAction] = Field( + actions: list[TriggerAction] = Field( default=[], title="Trigger actions", description="List of actions to execute when trigger matches (notification, sub_label, attribute).", @@ -243,7 +242,7 @@ class TriggerConfig(FrigateBaseModel): class CameraSemanticSearchConfig(FrigateBaseModel): - triggers: Dict[str, TriggerConfig] = Field( + triggers: dict[str, TriggerConfig] = Field( default={}, title="Triggers", description="Actions and matching criteria for camera-specific semantic search triggers.", @@ -307,7 +306,7 @@ class FaceRecognitionConfig(FrigateBaseModel): title="Blur confidence filter", description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", @@ -369,7 +368,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): title="Min plate length", description="Minimum number of characters a recognized plate must contain to be considered valid.", ) - format: Optional[str] = Field( + format: str | None = Field( default=None, title="Plate format regex", description="Optional regex to validate recognized plate strings against an expected format.", @@ -380,7 +379,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): description="Number of character mismatches allowed when comparing detected plates to known plates.", ge=0, ) - known_plates: Optional[Dict[str, List[str]]] = Field( + known_plates: dict[str, list[str]] | None = Field( default={}, title="Known plates", description="List of plates or regexes to specially track or alert on.", @@ -397,12 +396,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel): title="Save debug plates", description="Save plate crop images for debugging LPR performance.", ) - device: Optional[str] = Field( + device: str | None = Field( default=None, title="Device", description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", ) - replace_rules: List[ReplaceRule] = Field( + replace_rules: list[ReplaceRule] = Field( default_factory=list, title="Replacement rules", description="Regex replacement rules used to normalize detected plate strings before matching.", @@ -443,10 +442,10 @@ class CameraAudioTranscriptionConfig(FrigateBaseModel): title="Enable transcription", description="Enable or disable manually triggered audio event transcription.", ) - enabled_in_config: Optional[bool] = Field( + enabled_in_config: bool | None = Field( default=None, title="Original transcription state" ) - live_enabled: Optional[bool] = Field( + live_enabled: bool | None = Field( default=False, title="Live transcription", description="Enable streaming live transcription for audio as it is received.", diff --git a/frigate/config/config.py b/frigate/config/config.py index 1a12c20e51..b450d26e91 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -4,7 +4,7 @@ import io import json import logging import os -from typing import Any, Dict, Optional +from typing import Any, Self import numpy as np from pydantic import ( @@ -17,7 +17,6 @@ from pydantic import ( model_validator, ) from ruamel.yaml import YAML -from typing_extensions import Self from frigate.const import REGEX_JSON from frigate.detectors import DetectorConfig, ModelConfig @@ -174,7 +173,7 @@ class RuntimeMotionConfig(MotionConfig): class RuntimeFilterConfig(FilterConfig): """Runtime version of FilterConfig with rasterized masks.""" - rasterized_mask: Optional[np.ndarray] = Field(default=None, exclude=True) + rasterized_mask: np.ndarray | None = Field(default=None, exclude=True) def __init__(self, **config): frame_shape = config.get("frame_shape", (1, 1)) @@ -293,7 +292,7 @@ def verify_recording_segments_setup_with_reasonable_time( raise ValueError( f"Camera {camera_config.name} has no segment_time in \ recording output args, segment args are required for record." - ) + ) from None if int(record_args[seg_arg_index + 1]) > 60: raise ValueError( @@ -401,6 +400,9 @@ def verify_objects_track( ) camera_config.objects.track = valid_objects + for label in invalid_objects: + camera_config.objects.filters.pop(label, None) + def verify_lpr_and_face( frigate_config: FrigateConfig, camera_config: CameraConfig @@ -420,7 +422,7 @@ def verify_lpr_and_face( class FrigateConfig(FrigateBaseModel): - version: Optional[str] = Field( + version: str | None = Field( default=None, title="Current config version", description="Numeric or string version of the active configuration to help detect migrations or format changes.", @@ -496,7 +498,7 @@ class FrigateConfig(FrigateBaseModel): ) # Detector config - detectors: Dict[str, BaseDetectorConfig] = Field( + detectors: dict[str, BaseDetectorConfig] = Field( default=DEFAULT_DETECTORS, title="Detector hardware", description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", @@ -508,14 +510,14 @@ class FrigateConfig(FrigateBaseModel): ) # GenAI config (named provider configs: name -> GenAIConfig) - genai: Dict[str, GenAIConfig] = Field( + genai: dict[str, GenAIConfig] = Field( default_factory=dict, title="Generative AI configuration", description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.", ) # Camera config - cameras: Dict[str, CameraConfig] = Field(title="Cameras", description="Cameras") + cameras: dict[str, CameraConfig] = Field(title="Cameras", description="Cameras") audio: AudioConfig = Field( default_factory=AudioConfig, title="Audio detection", @@ -541,7 +543,7 @@ class FrigateConfig(FrigateBaseModel): title="Live playback", description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", ) - motion: Optional[MotionConfig] = Field( + motion: MotionConfig | None = Field( default=None, title="Motion detection", description="Default motion detection settings applied to cameras unless overridden per-camera.", @@ -599,19 +601,19 @@ class FrigateConfig(FrigateBaseModel): description="License plate recognition settings including detection thresholds, formatting, and known plates.", ) - camera_groups: Dict[str, CameraGroupConfig] = Field( + camera_groups: dict[str, CameraGroupConfig] = Field( default_factory=dict, title="Camera groups", description="Configuration for named camera groups used to organize cameras in the UI.", ) - profiles: Dict[str, ProfileDefinitionConfig] = Field( + profiles: dict[str, ProfileDefinitionConfig] = Field( default_factory=dict, title="Profiles", description="Named profile definitions with friendly names. Camera profiles must reference names defined here.", ) - active_profile: Optional[str] = Field( + active_profile: str | None = Field( default=None, title="Active profile", description="Currently active profile name. Runtime-only, not persisted in YAML.", @@ -1054,7 +1056,7 @@ class FrigateConfig(FrigateBaseModel): @field_validator("cameras") @classmethod - def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]): + def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]): zones = [zone for camera in v.values() for zone in camera.zones.keys()] for zone in zones: if zone in v.keys(): @@ -1136,7 +1138,7 @@ class FrigateConfig(FrigateBaseModel): @classmethod def parse_object( - cls, obj: Any, *, plus_api: Optional[PlusApi] = None, install: bool = False + cls, obj: Any, *, plus_api: PlusApi | None = None, install: bool = False ): return cls.model_validate( obj, context={"plus_api": plus_api, "install": install} diff --git a/frigate/config/logger.py b/frigate/config/logger.py index c8920a198a..906f75e4a3 100644 --- a/frigate/config/logger.py +++ b/frigate/config/logger.py @@ -1,5 +1,6 @@ +from typing import Self + from pydantic import Field, ValidationInfo, model_validator -from typing_extensions import Self from frigate.log import LogLevel, apply_log_levels diff --git a/frigate/config/mqtt.py b/frigate/config/mqtt.py index becbe7e69a..28c002a0de 100644 --- a/frigate/config/mqtt.py +++ b/frigate/config/mqtt.py @@ -1,7 +1,6 @@ -from typing import Optional +from typing import Self from pydantic import Field, ValidationInfo, model_validator -from typing_extensions import Self from frigate.const import FREQUENCY_STATS_POINTS @@ -43,33 +42,33 @@ class MqttConfig(FrigateBaseModel): title="Stats interval", description="Interval in seconds for publishing system and camera stats to MQTT.", ) - user: Optional[EnvString] = Field( + user: EnvString | None = Field( default=None, title="MQTT username", description="Optional MQTT username; can be provided via environment variables or secrets.", ) - password: Optional[EnvString] = Field( + password: EnvString | None = Field( default=None, title="MQTT password", description="Optional MQTT password; can be provided via environment variables or secrets.", validate_default=True, ) - tls_ca_certs: Optional[str] = Field( + tls_ca_certs: str | None = Field( default=None, title="TLS CA certs", description="Path to CA certificate for TLS connections to the broker (for self-signed certs).", ) - tls_client_cert: Optional[str] = Field( + tls_client_cert: str | None = Field( default=None, title="Client cert", description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", ) - tls_client_key: Optional[str] = Field( + tls_client_key: str | None = Field( default=None, title="Client key", description="Private key path for the client certificate.", ) - tls_insecure: Optional[bool] = Field( + tls_insecure: bool | None = Field( default=None, title="TLS insecure", description="Allow insecure TLS connections by skipping hostname verification (not recommended).", diff --git a/frigate/config/network.py b/frigate/config/network.py index f537c73b9d..7feaab0127 100644 --- a/frigate/config/network.py +++ b/frigate/config/network.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import Field from .base import FrigateBaseModel @@ -16,12 +14,12 @@ class IPv6Config(FrigateBaseModel): class ListenConfig(FrigateBaseModel): - internal: Union[int, str] = Field( + internal: int | str = Field( default=5000, title="Internal port", description="Internal listening port for Frigate (default 5000).", ) - external: Union[int, str] = Field( + external: int | str = Field( default=8971, title="External port", description="External listening port for Frigate (default 8971).", diff --git a/frigate/config/profile_manager.py b/frigate/config/profile_manager.py index e0a40ee353..cfd0dc9df5 100644 --- a/frigate/config/profile_manager.py +++ b/frigate/config/profile_manager.py @@ -3,9 +3,10 @@ import copy import json import logging -from datetime import datetime, timezone +from collections.abc import Callable +from datetime import UTC, datetime from pathlib import Path -from typing import Any, Callable, Optional +from typing import Any from frigate.config.camera.updater import ( CameraConfigUpdateEnum, @@ -169,9 +170,9 @@ class ProfileManager: def activate_profile( self, - profile_name: Optional[str], + profile_name: str | None, clear_runtime_overrides: bool = True, - ) -> Optional[str]: + ) -> str | None: """Activate a profile by name, or deactivate if None. Args: @@ -256,7 +257,7 @@ class ProfileManager: def _apply_profile_overrides( self, profile_name: str, changed: dict[str, set[str]] - ) -> Optional[str]: + ) -> str | None: """Apply profile overrides for all cameras that have the named profile.""" for cam_name, cam_config in self.config.cameras.items(): profile = cam_config.profiles.get(profile_name) @@ -358,14 +359,14 @@ class ProfileManager: retain=True, ) - def _persist_active_profile(self, profile_name: Optional[str]) -> None: + def _persist_active_profile(self, profile_name: str | None) -> None: """Persist the active profile state to disk as JSON.""" try: data = self._load_persisted_data() data["active"] = profile_name if profile_name is not None: data.setdefault("last_activated", {})[profile_name] = datetime.now( - timezone.utc + UTC ).timestamp() PERSISTENCE_FILE.write_text(json.dumps(data)) except OSError: @@ -384,7 +385,7 @@ class ProfileManager: return {"active": None, "last_activated": {}} @staticmethod - def load_persisted_profile() -> Optional[str]: + def load_persisted_profile() -> str | None: """Load the persisted active profile name from disk.""" data = ProfileManager._load_persisted_data() name = data.get("active") diff --git a/frigate/config/proxy.py b/frigate/config/proxy.py index 8c20c6e6dd..196110520b 100644 --- a/frigate/config/proxy.py +++ b/frigate/config/proxy.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field, field_validator from .base import FrigateBaseModel @@ -19,7 +17,7 @@ class HeaderMappingConfig(FrigateBaseModel): title="Role header", description="Header containing the authenticated user's role or groups from the upstream proxy.", ) - role_map: Optional[dict[str, list[str]]] = Field( + role_map: dict[str, list[str]] | None = Field( default_factory=dict, title=("Role mapping"), description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).", @@ -32,22 +30,22 @@ class ProxyConfig(FrigateBaseModel): title="Header mapping", description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", ) - logout_url: Optional[str] = Field( + logout_url: str | None = Field( default=None, title="Logout URL", description="URL to redirect users to when logging out via the proxy.", ) - auth_secret: Optional[EnvString] = Field( + auth_secret: EnvString | None = Field( default=None, title="Proxy secret", description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", ) - default_role: Optional[str] = Field( + default_role: str | None = Field( default="viewer", title="Default role", description="Default role assigned to proxy-authenticated users when no role mapping applies.", ) - separator: Optional[str] = Field( + separator: str | None = Field( default=",", title="Separator character", description="Character used to split multiple values provided in proxy headers.", diff --git a/frigate/config/telemetry.py b/frigate/config/telemetry.py index f85ff343f3..3c219d7460 100644 --- a/frigate/config/telemetry.py +++ b/frigate/config/telemetry.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import Field from .base import FrigateBaseModel @@ -23,7 +21,7 @@ class StatsConfig(FrigateBaseModel): title="Network bandwidth", description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", ) - intel_gpu_device: Optional[str] = Field( + intel_gpu_device: str | None = Field( default=None, title="Intel GPU device", description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", diff --git a/frigate/config/ui.py b/frigate/config/ui.py index 057a2b3336..5958c0e2c5 100644 --- a/frigate/config/ui.py +++ b/frigate/config/ui.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Optional from pydantic import Field @@ -20,7 +19,7 @@ class UnitSystemEnum(str, Enum): class UIConfig(FrigateBaseModel): - timezone: Optional[str] = Field( + timezone: str | None = Field( default=None, title="Timezone", description="Optional timezone to display across the UI (defaults to browser local time if unset).", diff --git a/frigate/data_processing/common/license_plate/mixin.py b/frigate/data_processing/common/license_plate/mixin.py index 7ebb464242..432787e40f 100644 --- a/frigate/data_processing/common/license_plate/mixin.py +++ b/frigate/data_processing/common/license_plate/mixin.py @@ -10,7 +10,7 @@ import random import re import string from pathlib import Path -from typing import Any, List, Tuple +from typing import Any import cv2 import numpy as np @@ -86,13 +86,15 @@ class LicensePlateProcessingMixin: self.similarity_threshold = 0.8 self.cluster_threshold = 0.85 - def _detect(self, image: np.ndarray) -> List[np.ndarray]: + def _detect(self, image: np.ndarray, debug_frame_id: int) -> list[np.ndarray]: """ Detect possible areas of text in the input image by first resizing and normalizing it, running a detection model, and filtering out low-probability regions. Args: image (np.ndarray): The input image in which license plates will be detected. + debug_frame_id (int): Shared id used to name debug images so all artifacts + from a single LPR pass share the same filename suffix. Returns: List[np.ndarray]: A list of bounding box coordinates representing detected license plates. @@ -106,9 +108,8 @@ class LicensePlateProcessingMixin: normalized_image = self._normalize_image(resized_image) if WRITE_DEBUG_IMAGES: - current_time = int(datetime.datetime.now().timestamp()) cv2.imwrite( - f"debug/frames/license_plate_resized_{current_time}.jpg", + f"debug/frames/license_plate_resized_{debug_frame_id}.jpg", resized_image, ) @@ -131,8 +132,8 @@ class LicensePlateProcessingMixin: return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type] def _classify( - self, images: List[np.ndarray] - ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]] | None: + self, images: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]] | None: """ Classify the orientation or category of each detected license plate. @@ -162,8 +163,8 @@ class LicensePlateProcessingMixin: return self._process_classification_output(images, outputs) def _recognize( - self, camera: str, images: List[np.ndarray] - ) -> Tuple[List[str], List[List[float]]]: + self, camera: str, images: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: """ Recognize the characters on the detected license plates using the recognition model. @@ -203,8 +204,8 @@ class LicensePlateProcessingMixin: return self.ctc_decoder(outputs) def _process_license_plate( - self, camera: str, id: str, image: np.ndarray - ) -> Tuple[List[str], List[List[float]], List[int]]: + self, camera: str, id: str, image: np.ndarray, debug_frame_id: int + ) -> tuple[list[str], list[list[float]], list[int]]: """ Complete pipeline for detecting, classifying, and recognizing license plates in the input image. Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom, @@ -214,6 +215,8 @@ class LicensePlateProcessingMixin: camera (str): Camera identifier. id (str): Event identifier. image (np.ndarray): The input image in which to detect, classify, and recognize license plates. + debug_frame_id (int): Shared id used to name debug images so all artifacts + from a single LPR pass share the same filename suffix. Returns: Tuple[List[str], List[List[float]], List[int]]: Detected license plate texts, character-level confidence scores for each plate (flattened into a single list per plate), and areas of the plates. @@ -227,7 +230,7 @@ class LicensePlateProcessingMixin: logger.debug("Model runners not loaded") return [], [], [] - boxes = self._detect(image) + boxes = self._detect(image, debug_frame_id) if len(boxes) == 0: logger.debug(f"{camera}: No boxes found by OCR detector model") return [], [], [] @@ -243,7 +246,6 @@ class LicensePlateProcessingMixin: boxes, plate_width=plate_width, gap_fraction=0.1 ) - current_time = int(datetime.datetime.now().timestamp()) if WRITE_DEBUG_IMAGES: debug_image = image.copy() for box in boxes: @@ -259,7 +261,7 @@ class LicensePlateProcessingMixin: ) cv2.imwrite( - f"debug/frames/license_plate_boxes_{current_time}.jpg", debug_image + f"debug/frames/license_plate_boxes_{debug_frame_id}.jpg", debug_image ) boxes = self._sort_boxes(list(boxes)) @@ -322,7 +324,7 @@ class LicensePlateProcessingMixin: if WRITE_DEBUG_IMAGES: for i, img in enumerate(group_plate_images): cv2.imwrite( - f"debug/frames/license_plate_cropped_{current_time}_{group_indices[i] + 1}.jpg", + f"debug/frames/license_plate_cropped_{debug_frame_id}_{group_indices[i] + 1}.jpg", img, ) @@ -335,7 +337,7 @@ class LicensePlateProcessingMixin: cv2.imwrite( os.path.join( CLIPS_DIR, - f"lpr/{camera}/{id}/{current_time}_{group_indices[i] + 1}.jpg", + f"lpr/{camera}/{id}/{debug_frame_id}_{group_indices[i] + 1}.jpg", ), img, ) @@ -467,11 +469,11 @@ class LicensePlateProcessingMixin: def _merge_nearby_boxes( self, - boxes: List[np.ndarray], + boxes: list[np.ndarray], plate_width: float, gap_fraction: float = 0.1, min_overlap_fraction: float = -0.2, - ) -> List[np.ndarray]: + ) -> list[np.ndarray]: """ Merge bounding boxes that are likely part of the same license plate based on proximity, with a dynamic max_gap based on the provided width of the entire license plate. @@ -553,7 +555,7 @@ class LicensePlateProcessingMixin: def _boxes_from_bitmap( self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int - ) -> Tuple[np.ndarray, List[float]]: + ) -> tuple[np.ndarray, list[float]]: """ Process the binary mask to extract bounding boxes and associated confidence scores. @@ -618,7 +620,7 @@ class LicensePlateProcessingMixin: return np.array(boxes, dtype="int32"), scores @staticmethod - def _get_min_boxes(contour: np.ndarray) -> Tuple[List[Tuple[float, float]], float]: + def _get_min_boxes(contour: np.ndarray) -> tuple[list[tuple[float, float]], float]: """ Calculate the minimum bounding box (rotated rectangle) for a given contour. @@ -657,7 +659,7 @@ class LicensePlateProcessingMixin: return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0] @staticmethod - def _expand_box(points: List[Tuple[float, float]]) -> np.ndarray: + def _expand_box(points: list[tuple[float, float]]) -> np.ndarray: """ Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio. @@ -675,7 +677,7 @@ class LicensePlateProcessingMixin: return expanded def _filter_polygon( - self, points: List[np.ndarray], shape: Tuple[int, int] + self, points: list[np.ndarray], shape: tuple[int, int] ) -> np.ndarray: """ Filter a set of polygons to include only valid ones that fit within an image shape @@ -837,8 +839,8 @@ class LicensePlateProcessingMixin: return padded_image def _process_classification_output( - self, images: List[np.ndarray], outputs: List[np.ndarray] - ) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]: + self, images: list[np.ndarray], outputs: list[np.ndarray] + ) -> tuple[list[np.ndarray], list[tuple[str, float]]]: """ Process the classification model output by matching labels with confidence scores. @@ -1093,8 +1095,8 @@ class LicensePlateProcessingMixin: return None # No detection above the threshold def _get_cluster_rep( - self, plates: List[dict] - ) -> Tuple[str, float, List[float], int]: + self, plates: list[dict] + ) -> tuple[str, float, list[float], int]: """ Cluster plate variants and select the representative from the best cluster. """ @@ -1199,6 +1201,7 @@ class LicensePlateProcessingMixin: self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps() camera = obj_data if dedicated_lpr else obj_data["camera"] current_time = int(datetime.datetime.now().timestamp()) + debug_frame_id = int(datetime.datetime.now().timestamp() * 1000) if not self.config.cameras[camera].lpr.enabled: return @@ -1214,7 +1217,7 @@ class LicensePlateProcessingMixin: if WRITE_DEBUG_IMAGES: cv2.imwrite( - f"debug/frames/dedicated_lpr_masked_{current_time}.jpg", + f"debug/frames/dedicated_lpr_masked_{debug_frame_id}.jpg", rgb, ) @@ -1326,7 +1329,7 @@ class LicensePlateProcessingMixin: if WRITE_DEBUG_IMAGES: cv2.imwrite( - f"debug/frames/car_frame_{current_time}.jpg", + f"debug/frames/car_frame_{debug_frame_id}.jpg", car, ) @@ -1454,7 +1457,7 @@ class LicensePlateProcessingMixin: if WRITE_DEBUG_IMAGES: cv2.imwrite( - f"debug/frames/license_plate_frame_{current_time}.jpg", + f"debug/frames/license_plate_frame_{debug_frame_id}.jpg", license_plate_frame, ) @@ -1464,7 +1467,7 @@ class LicensePlateProcessingMixin: # run detection, returns results sorted by confidence, best first start = datetime.datetime.now().timestamp() license_plates, confidences, areas = self._process_license_plate( - camera, id, license_plate_frame + camera, id, license_plate_frame, debug_frame_id ) self.plates_rec_second.update() self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start) @@ -1701,7 +1704,7 @@ class CTCDecoder: """ self.characters = [] if character_dict_path and os.path.exists(character_dict_path): - with open(character_dict_path, "r", encoding="utf-8") as f: + with open(character_dict_path, encoding="utf-8") as f: self.characters = ( ["blank"] + [line.strip() for line in f if line.strip()] + [" "] ) @@ -1809,8 +1812,8 @@ class CTCDecoder: self.char_map = {i: char for i, char in enumerate(self.characters)} def __call__( - self, outputs: List[np.ndarray] - ) -> Tuple[List[str], List[List[float]]]: + self, outputs: list[np.ndarray] + ) -> tuple[list[str], list[list[float]]]: """ Decode a batch of model outputs into character sequences and their confidence scores. diff --git a/frigate/data_processing/post/audio_transcription.py b/frigate/data_processing/post/audio_transcription.py index dbeb210287..16895dc873 100644 --- a/frigate/data_processing/post/audio_transcription.py +++ b/frigate/data_processing/post/audio_transcription.py @@ -4,7 +4,7 @@ import logging import os import threading import time -from typing import Any, Optional +from typing import Any from peewee import DoesNotExist @@ -142,7 +142,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): except Exception as e: logger.error(f"Error in audio transcription post-processing: {e}") - def __transcribe_audio(self, audio_data: bytes) -> Optional[str]: + def __transcribe_audio(self, audio_data: bytes) -> str | None: """Transcribe WAV audio data using faster-whisper.""" if not self.recognizer: logger.debug("Recognizer not initialized") @@ -168,8 +168,9 @@ class AudioTranscriptionPostProcessor(PostProcessorApi): return None logger.debug( - "Detected language '%s' with probability %f" - % (info.language, info.language_probability) + "Detected language '%s' with probability %f", + info.language, + info.language_probability, ) return text diff --git a/frigate/data_processing/post/license_plate.py b/frigate/data_processing/post/license_plate.py index aa89aeb12b..55f863cf0b 100644 --- a/frigate/data_processing/post/license_plate.py +++ b/frigate/data_processing/post/license_plate.py @@ -102,10 +102,8 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi): Recordings.start_time, ) .where( - ( - (frame_time >= Recordings.start_time) - & (frame_time <= Recordings.end_time) - ) + (frame_time >= Recordings.start_time) + & (frame_time <= Recordings.end_time) ) .where(Recordings.camera == camera_name) .order_by(Recordings.start_time.desc()) diff --git a/frigate/data_processing/post/semantic_trigger.py b/frigate/data_processing/post/semantic_trigger.py index 08f8a2e762..e2b305ea2d 100644 --- a/frigate/data_processing/post/semantic_trigger.py +++ b/frigate/data_processing/post/semantic_trigger.py @@ -55,7 +55,7 @@ class SemanticTriggerProcessor(PostProcessorApi): # load stats from disk try: - with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f: + with open(os.path.join(CONFIG_DIR, ".search_stats.json")) as f: data = json.loads(f.read()) self.thumb_stats.from_dict(data["thumb_stats"]) self.desc_stats.from_dict(data["desc_stats"]) diff --git a/frigate/data_processing/real_time/api.py b/frigate/data_processing/real_time/api.py index b9b7ba26ee..98efb532b4 100644 --- a/frigate/data_processing/real_time/api.py +++ b/frigate/data_processing/real_time/api.py @@ -4,9 +4,10 @@ import logging import threading from abc import ABC, abstractmethod from collections import deque +from collections.abc import Callable from concurrent.futures import Future from queue import Empty, Full, Queue -from typing import Any, Callable +from typing import Any import numpy as np diff --git a/frigate/data_processing/real_time/audio_transcription.py b/frigate/data_processing/real_time/audio_transcription.py index 3d1536f730..0e1842b775 100644 --- a/frigate/data_processing/real_time/audio_transcription.py +++ b/frigate/data_processing/real_time/audio_transcription.py @@ -4,7 +4,7 @@ import logging import os import queue import threading -from typing import Any, Optional +from typing import Any import numpy as np @@ -75,9 +75,7 @@ class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi): f"Failed to initialize live streaming audio transcription: {e}" ) - def __process_audio_stream( - self, audio_data: np.ndarray - ) -> Optional[tuple[str, bool]]: + def __process_audio_stream(self, audio_data: np.ndarray) -> tuple[str, bool] | None: if ( self.model_runner.model is None and self.config.audio_transcription.model_size == "small" diff --git a/frigate/data_processing/real_time/face.py b/frigate/data_processing/real_time/face.py index c5c4ec56f0..83c8a2e55a 100644 --- a/frigate/data_processing/real_time/face.py +++ b/frigate/data_processing/real_time/face.py @@ -7,7 +7,7 @@ import logging import os import shutil from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -219,7 +219,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi): logger.debug("Not processing due to hitting max rec attempts.") return - face: Optional[dict[str, Any]] = None + face: dict[str, Any] | None = None if self.requires_face_detection: logger.debug("Running manual face detection.") diff --git a/frigate/data_processing/real_time/whisper_online.py b/frigate/data_processing/real_time/whisper_online.py index 024b19fba3..066303c93e 100644 --- a/frigate/data_processing/real_time/whisper_online.py +++ b/frigate/data_processing/real_time/whisper_online.py @@ -1053,7 +1053,7 @@ if __name__ == "__main__": SAMPLING_RATE = 16000 duration = len(load_audio(audio_path)) / SAMPLING_RATE - logger.info("Audio duration is: %2.2f seconds" % duration) + logger.info("Audio duration is: %2.2f seconds", duration) asr, online = asr_factory(args, logfile=logfile) if args.vac: diff --git a/frigate/db/sqlitevecq.py b/frigate/db/sqlitevecq.py index a72e99b6a2..137fb51451 100644 --- a/frigate/db/sqlitevecq.py +++ b/frigate/db/sqlitevecq.py @@ -1,9 +1,11 @@ -import re import sqlite3 from typing import Any +import regex from playhouse.sqliteq import SqliteQueueDatabase +REGEXP_TIMEOUT_SECONDS = 1.0 + class SqliteVecQueueDatabase(SqliteQueueDatabase): def __init__( @@ -34,8 +36,10 @@ class SqliteVecQueueDatabase(SqliteQueueDatabase): if item is None: return False try: - return re.search(expr, item) is not None - except re.error: + return ( + regex.search(expr, item, timeout=REGEXP_TIMEOUT_SECONDS) is not None + ) + except (regex.error, TimeoutError): return False conn.create_function("REGEXP", 2, regexp) diff --git a/frigate/debug_replay.py b/frigate/debug_replay.py index 956bc20012..f8e801c3f3 100644 --- a/frigate/debug_replay.py +++ b/frigate/debug_replay.py @@ -21,8 +21,6 @@ from frigate.config.camera.updater import ( CameraConfigUpdateTopic, ) from frigate.const import ( - CLIPS_DIR, - RECORD_DIR, REPLAY_CAMERA_PREFIX, REPLAY_DIR, THUMB_DIR, @@ -131,7 +129,7 @@ class DebugReplayManager: config_file = find_config_file() yaml_parser = YAML() - with open(config_file, "r") as f: + with open(config_file) as f: config_data = yaml_parser.load(f) if "cameras" not in config_data or config_data["cameras"] is None: @@ -331,12 +329,14 @@ def cleanup_replay_cameras() -> None: """ stale_cameras: set[str] = set() - # Scan filesystem for leftover replay artifacts to derive camera names - for dir_path in [RECORD_DIR, CLIPS_DIR, THUMB_DIR]: - if os.path.isdir(dir_path): - for entry in os.listdir(dir_path): - if entry.startswith(REPLAY_CAMERA_PREFIX): - stale_cameras.add(entry) + # Derive stale camera names from THUMB_DIR (per-camera dirs) and + # REPLAY_DIR (the session's source clip); both listings are bounded by + # camera count. cleanup_camera_files below removes any remaining + # per-camera artifacts (snapshots, thumbnails, LPR images, etc.) by name. + if os.path.isdir(THUMB_DIR): + for entry in os.listdir(THUMB_DIR): + if entry.startswith(REPLAY_CAMERA_PREFIX): + stale_cameras.add(entry) if os.path.isdir(REPLAY_DIR): for entry in os.listdir(REPLAY_DIR): diff --git a/frigate/detectors/detection_api.py b/frigate/detectors/detection_api.py index 4f03f28aa4..b03d607d5a 100644 --- a/frigate/detectors/detection_api.py +++ b/frigate/detectors/detection_api.py @@ -1,6 +1,5 @@ import logging from abc import ABC, abstractmethod -from typing import List import numpy as np @@ -11,7 +10,7 @@ logger = logging.getLogger(__name__) class DetectionApi(ABC): type_key: str - supported_models: List[ModelTypeEnum] + supported_models: list[ModelTypeEnum] @abstractmethod def __init__(self, detector_config: BaseDetectorConfig): diff --git a/frigate/detectors/detection_runners.py b/frigate/detectors/detection_runners.py index ee465b3d51..ee30f4fead 100644 --- a/frigate/detectors/detection_runners.py +++ b/frigate/detectors/detection_runners.py @@ -491,7 +491,7 @@ class RKNNModelRunner(BaseModelRunner): except ImportError: logger.error("RKNN Lite not available") - raise ImportError("RKNN Lite not available") + raise ImportError("RKNN Lite not available") from None except Exception as e: logger.error(f"Error loading RKNN model: {e}") raise diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index 5071e3a741..788fb58a79 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -3,7 +3,7 @@ import json import logging import os from enum import Enum -from typing import Any, Dict, Optional, Tuple +from typing import Any import requests from pydantic import BaseModel, ConfigDict, Field @@ -45,12 +45,12 @@ class ModelTypeEnum(str, Enum): class ModelConfig(BaseModel): - path: Optional[str] = Field( + path: str | None = Field( None, title="Custom object detector model path", description="Path to a custom detection model file (or plus:// for Frigate+ models).", ) - labelmap_path: Optional[str] = Field( + labelmap_path: str | None = Field( None, title="Label map for custom object detector", description="Path to a labelmap file that maps numeric classes to string labels for the detector.", @@ -65,12 +65,12 @@ class ModelConfig(BaseModel): title="Object detection model input height", description="Height of the model input tensor in pixels.", ) - labelmap: Dict[int, str] = Field( + labelmap: dict[int, str] = Field( default_factory=dict, title="Labelmap customization", description="Overrides or remapping entries to merge into the standard labelmap.", ) - attributes_map: Dict[str, list[str]] = Field( + attributes_map: dict[str, list[str]] = Field( default=DEFAULT_ATTRIBUTE_LABEL_MAP, title="Map of object labels to their attribute labels", description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", @@ -95,18 +95,18 @@ class ModelConfig(BaseModel): title="Object Detection Model Type", description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", ) - _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr() - _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr() + _merged_labelmap: dict[int, str] | None = PrivateAttr() + _colormap: dict[int, tuple[int, int, int]] = PrivateAttr() _all_attributes: list[str] = PrivateAttr() _all_attribute_logos: list[str] = PrivateAttr() _model_hash: str = PrivateAttr() @property - def merged_labelmap(self) -> Dict[int, str]: + def merged_labelmap(self) -> dict[int, str]: return self._merged_labelmap @property - def colormap(self) -> Dict[int, Tuple[int, int, int]]: + def colormap(self) -> dict[int, tuple[int, int, int]]: return self._colormap @property @@ -171,7 +171,7 @@ class ModelConfig(BaseModel): with open(model_info_path, "w") as f: json.dump(model_info, f) else: - with open(model_info_path, "r") as f: + with open(model_info_path) as f: model_info: dict[str, Any] = json.load(f) if detector and detector not in model_info["supportedDetectors"]: @@ -240,12 +240,12 @@ class BaseDetectorConfig(BaseModel): title="Detector Type", description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", ) - model: Optional[ModelConfig] = Field( + model: ModelConfig | None = Field( default=None, title="Detector specific model configuration", description="Detector-specific model configuration options (path, input size, etc.).", ) - model_path: Optional[str] = Field( + model_path: str | None = Field( default=None, title="Detector specific model path", description="File path to the detector model binary if required by the chosen detector.", diff --git a/frigate/detectors/detector_types.py b/frigate/detectors/detector_types.py index 418fcd625c..42129c5945 100644 --- a/frigate/detectors/detector_types.py +++ b/frigate/detectors/detector_types.py @@ -2,10 +2,9 @@ import importlib import logging import pkgutil from enum import Enum -from typing import Union +from typing import Annotated, Union from pydantic import Field -from typing_extensions import Annotated from . import plugins from .detection_api import DetectionApi @@ -37,6 +36,6 @@ class StrEnum(str, Enum): DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types}) DetectorConfig = Annotated[ - Union[tuple(BaseDetectorConfig.__subclasses__())], + Union[tuple(BaseDetectorConfig.__subclasses__())], # noqa: UP007 Field(discriminator="type"), ] diff --git a/frigate/detectors/plugins/axengine.py b/frigate/detectors/plugins/axengine.py index 383fcd0bf9..43a1fffe4d 100644 --- a/frigate/detectors/plugins/axengine.py +++ b/frigate/detectors/plugins/axengine.py @@ -39,8 +39,7 @@ class Axengine(DetectionApi): try: import axengine as axe except ModuleNotFoundError: - raise ImportError("AXEngine is not installed.") - return + raise ImportError("AXEngine is not installed.") from None logger.info("__init__ axengine") super().__init__(config) diff --git a/frigate/detectors/plugins/cpu_tfl.py b/frigate/detectors/plugins/cpu_tfl.py index 2224a2bdad..d8cfe33e0d 100644 --- a/frigate/detectors/plugins/cpu_tfl.py +++ b/frigate/detectors/plugins/cpu_tfl.py @@ -1,7 +1,7 @@ import logging +from typing import Literal from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/deepstack.py b/frigate/detectors/plugins/deepstack.py index 9b5fcd5af5..e87b07c444 100644 --- a/frigate/detectors/plugins/deepstack.py +++ b/frigate/detectors/plugins/deepstack.py @@ -1,11 +1,11 @@ import io import logging +from typing import Literal import numpy as np import requests from PIL import Image from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/degirum.py b/frigate/detectors/plugins/degirum.py index 5afb32a3ad..9aaf0e6dc9 100644 --- a/frigate/detectors/plugins/degirum.py +++ b/frigate/detectors/plugins/degirum.py @@ -1,9 +1,9 @@ import logging import queue +from typing import Literal import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -46,7 +46,7 @@ class DGDetector(DetectionApi): try: import degirum as dg except ModuleNotFoundError: - raise ImportError("Unable to import DeGirum detector.") + raise ImportError("Unable to import DeGirum detector.") from None self._queue = queue.Queue() self._zoo = dg.connect( diff --git a/frigate/detectors/plugins/edgetpu_tfl.py b/frigate/detectors/plugins/edgetpu_tfl.py index 02bd9f5ec5..c4ea485d57 100644 --- a/frigate/detectors/plugins/edgetpu_tfl.py +++ b/frigate/detectors/plugins/edgetpu_tfl.py @@ -1,11 +1,11 @@ import logging import math import os +from typing import Literal import cv2 import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum diff --git a/frigate/detectors/plugins/hailo8l.py b/frigate/detectors/plugins/hailo8l.py index bbe84d52f0..63a3c9a2a8 100755 --- a/frigate/detectors/plugins/hailo8l.py +++ b/frigate/detectors/plugins/hailo8l.py @@ -4,12 +4,11 @@ import subprocess import threading import urllib.request from functools import partial -from typing import Dict, List, Optional, Tuple +from typing import Literal import cv2 import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.const import MODEL_CACHE_DIR from frigate.detectors.detection_api import DetectionApi @@ -83,8 +82,8 @@ class HailoAsyncInference: input_store: RequestStore, output_store: ResponseStore, batch_size: int = 1, - input_type: Optional[str] = None, - output_type: Optional[Dict[str, str]] = None, + input_type: str | None = None, + output_type: dict[str, str] | None = None, send_original_frame: bool = False, ) -> None: # when importing hailo it activates the driver @@ -125,9 +124,9 @@ class HailoAsyncInference: def callback( self, completion_info, - bindings_list: List, - input_batch: List, - request_ids: List[int], + bindings_list: list, + input_batch: list, + request_ids: list[int], ): if completion_info.exception: logger.error(f"Inference error: {completion_info.exception}") @@ -163,7 +162,7 @@ class HailoAsyncInference: } return configured_infer_model.create_bindings(output_buffers=output_buffers) - def get_input_shape(self) -> Tuple[int, ...]: + def get_input_shape(self) -> tuple[int, ...]: return self.hef.get_input_vstream_infos()[0].shape def run(self) -> None: @@ -304,7 +303,7 @@ class HailoDetector(DetectionApi): urllib.request.urlretrieve(url, destination) logger.debug(f"Downloaded model to {destination}") except Exception as e: - raise RuntimeError(f"Failed to download model from {url}: {str(e)}") + raise RuntimeError(f"Failed to download model from {url}: {str(e)}") from e def check_and_prepare(self) -> str: if not os.path.exists(self.cache_dir): @@ -350,7 +349,7 @@ class HailoDetector(DetectionApi): if not self.inference_thread.is_alive(): raise RuntimeError( "HailoRT inference thread has stopped, restart required." - ) + ) from None return np.zeros((20, 6), dtype=np.float32) diff --git a/frigate/detectors/plugins/memryx.py b/frigate/detectors/plugins/memryx.py index 2c03d14a49..88537273ce 100644 --- a/frigate/detectors/plugins/memryx.py +++ b/frigate/detectors/plugins/memryx.py @@ -5,11 +5,11 @@ import shutil import urllib.request import zipfile from queue import Queue +from typing import Literal import cv2 import numpy as np from pydantic import BaseModel, ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import ( @@ -61,7 +61,7 @@ class MemryXDetector(DetectionApi): except ModuleNotFoundError: raise ImportError( "MemryX SDK is not installed. Install it and set up MIX environment." - ) + ) from None return # Initialize stop_event as None, will be set later by set_stop_event() diff --git a/frigate/detectors/plugins/onnx.py b/frigate/detectors/plugins/onnx.py index b9aa00fbdb..cbf189916b 100644 --- a/frigate/detectors/plugins/onnx.py +++ b/frigate/detectors/plugins/onnx.py @@ -1,8 +1,8 @@ import logging +from typing import Literal import numpy as np from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detection_runners import get_optimized_runner diff --git a/frigate/detectors/plugins/openvino.py b/frigate/detectors/plugins/openvino.py index 1e9fb1ab10..5bd55d4a6b 100644 --- a/frigate/detectors/plugins/openvino.py +++ b/frigate/detectors/plugins/openvino.py @@ -1,9 +1,9 @@ import logging +from typing import Literal import numpy as np import openvino as ov from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detection_runners import OpenVINOModelRunner diff --git a/frigate/detectors/plugins/rknn.py b/frigate/detectors/plugins/rknn.py index 15ab93dcbc..7bf5e03c15 100644 --- a/frigate/detectors/plugins/rknn.py +++ b/frigate/detectors/plugins/rknn.py @@ -90,7 +90,7 @@ class Rknn(DetectionApi): with open("/proc/device-tree/compatible") as file: soc = file.read().split(",")[-1].strip("\x00") except FileNotFoundError: - raise Exception("Make sure to run docker in privileged mode.") + raise Exception("Make sure to run docker in privileged mode.") from None if soc not in SUPPORTED_RK_SOCS: raise Exception( diff --git a/frigate/detectors/plugins/synaptics.py b/frigate/detectors/plugins/synaptics.py index e6983a29c3..6d0ac7f521 100644 --- a/frigate/detectors/plugins/synaptics.py +++ b/frigate/detectors/plugins/synaptics.py @@ -1,9 +1,9 @@ import logging import os +from typing import Literal import numpy as np from pydantic import ConfigDict -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import ( diff --git a/frigate/detectors/plugins/teflon_tfl.py b/frigate/detectors/plugins/teflon_tfl.py index 370d08817c..7dd44e6e89 100644 --- a/frigate/detectors/plugins/teflon_tfl.py +++ b/frigate/detectors/plugins/teflon_tfl.py @@ -1,7 +1,7 @@ import logging +from typing import Literal from pydantic import ConfigDict -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig diff --git a/frigate/detectors/plugins/tensorrt.py b/frigate/detectors/plugins/tensorrt.py index 087331a2dc..6b39212804 100644 --- a/frigate/detectors/plugins/tensorrt.py +++ b/frigate/detectors/plugins/tensorrt.py @@ -14,8 +14,9 @@ try: except ModuleNotFoundError: TRT_SUPPORT = False +from typing import Literal + from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -58,7 +59,7 @@ class TensorRTDetectorConfig(BaseDetectorConfig): ) -class HostDeviceMem(object): +class HostDeviceMem: """Simple helper data class that's a little nicer to use than a 2-tuple.""" def __init__(self, host_mem, device_mem, nbytes, size): diff --git a/frigate/detectors/plugins/zmq_ipc.py b/frigate/detectors/plugins/zmq_ipc.py index b0e568eff0..cc9a538c81 100644 --- a/frigate/detectors/plugins/zmq_ipc.py +++ b/frigate/detectors/plugins/zmq_ipc.py @@ -1,12 +1,11 @@ import json import logging import os -from typing import Any, List +from typing import Any, Literal import numpy as np import zmq from pydantic import ConfigDict, Field -from typing_extensions import Literal from frigate.detectors.detection_api import DetectionApi from frigate.detectors.detector_config import BaseDetectorConfig @@ -274,7 +273,7 @@ class ZmqIpcDetector(DetectionApi): } return json.dumps(header).encode("utf-8") - def _decode_response(self, frames: List[bytes]) -> np.ndarray: + def _decode_response(self, frames: list[bytes]) -> np.ndarray: try: if len(frames) == 1: # Single-frame raw float32 (20x6) diff --git a/frigate/embeddings/__init__.py b/frigate/embeddings/__init__.py index 7e54d97036..bf74df6811 100644 --- a/frigate/embeddings/__init__.py +++ b/frigate/embeddings/__init__.py @@ -8,7 +8,7 @@ import sys import threading from json.decoder import JSONDecodeError from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Union +from typing import Any import regex from pathvalidate import ValidationError, sanitize_filename @@ -73,7 +73,7 @@ class EmbeddingsContext: # load stats from disk stats_file = os.path.join(CONFIG_DIR, ".search_stats.json") try: - with open(stats_file, "r") as f: + with open(stats_file) as f: data = json.loads(f.read()) self.thumb_stats.from_dict(data["thumb_stats"]) self.desc_stats.from_dict(data["desc_stats"]) @@ -98,7 +98,7 @@ class EmbeddingsContext: self.requestor.stop() def search_thumbnail( - self, query: Union[Event, str], event_ids: list[str] = None + self, query: Event | str, event_ids: list[str] = None ) -> list[tuple[str, float]]: if query.__class__ == Event: cursor = self.db.execute_sql( @@ -255,7 +255,7 @@ class EmbeddingsContext: sanitized_old_name = sanitize_filename(old_name, replacement_text="_") sanitized_new_name = sanitize_filename(new_name, replacement_text="_") except ValidationError as e: - raise ValueError(f"Invalid face name: {str(e)}") + raise ValueError(f"Invalid face name: {str(e)}") from e if not regex.match(valid_name_pattern, old_name): raise ValueError(f"Invalid old face name: {old_name}") diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 52bdf5d915..a9b9b837b7 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -200,6 +200,9 @@ class EmbeddingMaintainer(threading.Thread): ) for model_config in self.config.classification.custom.values(): + if not model_config.enabled: + continue + self.realtime_processors.append( CustomStateClassificationProcessor( self.config, model_config, self.requestor, self.metrics @@ -332,6 +335,25 @@ class EmbeddingMaintainer(threading.Thread): for processor in self.post_processors: processor.update_config(topic, payload) + def _remove_custom_classification_processor(self, model_name: str) -> None: + """Shut down and drop any running processor for a custom model.""" + remaining = [] + for processor in self.realtime_processors: + if ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name + ): + processor.shutdown() + else: + remaining.append(processor) + self.realtime_processors = remaining + def _handle_custom_classification_update( self, topic: str, model_config: Any ) -> None: @@ -339,23 +361,7 @@ class EmbeddingMaintainer(threading.Thread): model_name = topic.split("/")[-1] if model_config is None: - remaining = [] - for processor in self.realtime_processors: - if ( - isinstance( - processor, - ( - CustomStateClassificationProcessor, - CustomObjectClassificationProcessor, - ), - ) - and processor.model_config.name == model_name - ): - processor.shutdown() - else: - remaining.append(processor) - self.realtime_processors = remaining - + self._remove_custom_classification_processor(model_name) logger.info( f"Successfully removed classification processor for model: {model_name}" ) @@ -363,20 +369,29 @@ class EmbeddingMaintainer(threading.Thread): self.config.classification.custom[model_name] = model_config - # Check if processor already exists + # A disabled model must not run; tear down any existing processor and + # do not register a new one. + if not model_config.enabled: + self._remove_custom_classification_processor(model_name) + logger.info(f"Disabled classification processor for model: {model_name}") + return + for processor in self.realtime_processors: - if isinstance( - processor, - ( - CustomStateClassificationProcessor, - CustomObjectClassificationProcessor, - ), + if ( + isinstance( + processor, + ( + CustomStateClassificationProcessor, + CustomObjectClassificationProcessor, + ), + ) + and processor.model_config.name == model_name ): - if processor.model_config.name == model_name: - logger.debug( - f"Classification processor for model {model_name} already exists, skipping" - ) - return + processor.model_config = model_config + logger.debug( + f"Updated config for classification processor: {model_name}" + ) + return if model_config.state_config is not None: processor = CustomStateClassificationProcessor( @@ -435,7 +450,7 @@ class EmbeddingMaintainer(threading.Thread): logger.error(f"No processor handled the topic {topic}") return None except Exception as e: - logger.error(f"Unable to handle embeddings request {e}", exc_info=True) + logger.exception(f"Unable to handle embeddings request {e}") self.embeddings_responder.check_for_request(_handle_request) @@ -702,7 +717,11 @@ class EmbeddingMaintainer(threading.Thread): and "license_plate" not in camera_config.objects.track ) - if not dedicated_lpr_enabled and len(self.config.classification.custom) == 0: + has_enabled_custom = any( + c.enabled for c in self.config.classification.custom.values() + ) + + if not dedicated_lpr_enabled and not has_enabled_custom: # no active features that use this data return diff --git a/frigate/embeddings/onnx/base_embedding.py b/frigate/embeddings/onnx/base_embedding.py index c0bd58475b..5b514d1f60 100644 --- a/frigate/embeddings/onnx/base_embedding.py +++ b/frigate/embeddings/onnx/base_embedding.py @@ -57,6 +57,12 @@ class BaseEmbedding(ABC): def _preprocess_inputs(self, raw_inputs: Any) -> Any: pass + @staticmethod + def _bgr_to_rgb(frame: Any) -> Any: + if isinstance(frame, np.ndarray) and frame.ndim == 3: + return np.ascontiguousarray(frame[:, :, ::-1]) + return frame + def _process_image(self, image, output: str = "RGB") -> Image.Image: if isinstance(image, str): if image.startswith("http"): diff --git a/frigate/embeddings/onnx/face_embedding.py b/frigate/embeddings/onnx/face_embedding.py index 75dfedc94d..31d2bd2066 100644 --- a/frigate/embeddings/onnx/face_embedding.py +++ b/frigate/embeddings/onnx/face_embedding.py @@ -73,7 +73,7 @@ class FaceNetEmbedding(BaseEmbedding): self.tensor_output_details = self.runner.get_output_details() def _preprocess_inputs(self, raw_inputs): - pil = self._process_image(raw_inputs[0]) + pil = self._process_image(self._bgr_to_rgb(raw_inputs[0])) # handle images larger than input size width, height = pil.size @@ -159,7 +159,7 @@ class ArcfaceEmbedding(BaseEmbedding): ) def _preprocess_inputs(self, raw_inputs): - pil = self._process_image(raw_inputs[0]) + pil = self._process_image(self._bgr_to_rgb(raw_inputs[0])) # handle images larger than input size width, height = pil.size diff --git a/frigate/events/audio.py b/frigate/events/audio.py index d44521787c..9af1bd26b2 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -7,7 +7,7 @@ import threading import time from multiprocessing.managers import DictProxy from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Tuple +from typing import Any import numpy as np @@ -272,7 +272,7 @@ class AudioEventMaintainer(threading.Thread): self.camera_metrics[self.camera_config.name].audio_rms.value = rms self.camera_metrics[self.camera_config.name].audio_dBFS.value = dBFS - audio_detections: list[Tuple[str, float]] = [] + audio_detections: list[tuple[str, float]] = [] # only run audio detection when volume is above min_volume if rms >= self.camera_config.audio.min_volume: @@ -323,7 +323,7 @@ class AudioEventMaintainer(threading.Thread): else: self.transcription_processor.check_unload_model() - def calculate_audio_levels(self, audio_as_float: np.ndarray) -> Tuple[float, float]: + def calculate_audio_levels(self, audio_as_float: np.ndarray) -> tuple[float, float]: # Calculate RMS (Root-Mean-Square) which represents the average signal amplitude # Note: np.float32 isn't serializable, we must use np.float64 to publish the message rms = np.sqrt(np.mean(np.absolute(np.square(audio_as_float)))) diff --git a/frigate/events/maintainer.py b/frigate/events/maintainer.py index 80bdaccd3b..169b2139ea 100644 --- a/frigate/events/maintainer.py +++ b/frigate/events/maintainer.py @@ -2,7 +2,7 @@ import logging import threading from multiprocessing import Queue from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Dict +from typing import Any from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber from frigate.config import FrigateConfig @@ -76,7 +76,7 @@ class EventProcessor(threading.Thread): super().__init__(name="event_processor") self.config = config self.timeline_queue = timeline_queue - self.events_in_process: Dict[str, dict[str, Any]] = {} + self.events_in_process: dict[str, dict[str, Any]] = {} self.stop_event = stop_event self.event_receiver = EventUpdateSubscriber() diff --git a/frigate/ffmpeg_presets.py b/frigate/ffmpeg_presets.py index 4ebbd6c801..445cfd4df3 100644 --- a/frigate/ffmpeg_presets.py +++ b/frigate/ffmpeg_presets.py @@ -3,7 +3,7 @@ import logging import os from enum import Enum -from typing import Any, Optional +from typing import Any from frigate.const import ( FFMPEG_HVC1_ARGS, @@ -150,7 +150,11 @@ PRESETS_HW_ACCEL_SCALE["preset-rk-h265"] = PRESETS_HW_ACCEL_SCALE[FFMPEG_HWACCEL PRESETS_HW_ACCEL_ENCODE_BIRDSEYE = { "preset-rpi-64-h264": "{0} -hide_banner {1} -c:v h264_v4l2m2m {2}", "preset-rpi-64-h265": "{0} -hide_banner {1} -c:v hevc_v4l2m2m {2}", - FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", + # -vaapi_device is required in addition to -hwaccel_device: this is the only + # birdseye preset that uses hwupload, and ffmpeg 8 initializes filters before + # the decoder creates a device, so hwupload cannot see an -hwaccel_device one. + # See https://github.com/AlexxIT/go2rtc/issues/1984 + FFMPEG_HWACCEL_VAAPI: "{0} -hide_banner -vaapi_device {3} -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_device {3} {1} -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload {2}", "preset-intel-qsv-h264": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 {2}", "preset-intel-qsv-h265": "{0} -hide_banner {1} -c:v h264_qsv -g 50 -bf 0 -profile:v main -level:v 4.1 -async_depth:v 1 {2}", FFMPEG_HWACCEL_NVIDIA: "{0} -hide_banner {1} -c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll {2}", @@ -215,7 +219,7 @@ def parse_preset_hardware_acceleration_decode( width: int, height: int, gpu: int, -) -> Optional[list[str]]: +) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None @@ -420,7 +424,7 @@ PRESETS_INPUT = { } -def parse_preset_input(arg: Any, detect_fps: int) -> Optional[list[str]]: +def parse_preset_input(arg: Any, detect_fps: int) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None @@ -530,9 +534,7 @@ PRESETS_RECORD_OUTPUT = { } -def parse_preset_output_record( - arg: Any, force_record_hvc1: bool -) -> Optional[list[str]]: +def parse_preset_output_record(arg: Any, force_record_hvc1: bool) -> list[str] | None: """Return the correct preset if in preset format otherwise return None.""" if not isinstance(arg, str): return None diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index bca5e6d691..3aca4a8fb4 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -5,7 +5,9 @@ import json import logging import os import re -from typing import Any, AsyncGenerator, Callable, Optional +import time +from collections.abc import AsyncGenerator, Callable +from typing import Any import numpy as np from pydantic import ValidationError @@ -50,6 +52,10 @@ def register_genai_provider(key: GenAIProviderEnum) -> Callable: class GenAIClient: """Generative AI client for Frigate.""" + # Minimum seconds between re-initialization attempts when the provider was + # offline at startup + REINIT_INTERVAL = 60.0 + def __init__( self, genai_config: GenAIConfig, @@ -60,6 +66,34 @@ class GenAIClient: self.timeout = timeout self.validate_model = validate_model self.provider = self._init_provider() + self._last_init_attempt = time.monotonic() + + def ensure_provider(self) -> bool: + """Ensure a provider is available, retrying initialization if needed. + + Providers can fail to initialize at startup when their backing service + isn't online yet (common when both are started together). This retries + ``_init_provider`` lazily — throttled to ``REINIT_INTERVAL`` — so the + client recovers on its own once the service is reachable, without a + config reload. + + Returns True if a provider is available. + """ + if self.provider is not None: + return True + + now = time.monotonic() + if now - self._last_init_attempt < self.REINIT_INTERVAL: + return False + + self._last_init_attempt = now + self.provider = self._init_provider() + if self.provider is not None: + logger.info( + "GenAI provider %s is now available", + self.genai_config.provider, + ) + return self.provider is not None def generate_review_description( self, @@ -202,7 +236,7 @@ class GenAIClient: camera_config: CameraConfig, thumbnails: list[bytes], event: Event, - ) -> Optional[str]: + ) -> str | None: """Generate a description for the frame.""" try: prompt = build_object_description_prompt(camera_config, event) @@ -221,9 +255,9 @@ class GenAIClient: self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to the provider. ``enable_thinking`` is honored only by providers that report @@ -247,6 +281,11 @@ class GenAIClient: """Whether the configured model exposes a per-request thinking toggle.""" return False + @property + def supports_embeddings(self) -> bool: + """Whether the configured model can generate embeddings via embed().""" + return False + def list_models(self) -> list[str]: """Return the list of model names available from this provider. @@ -288,9 +327,9 @@ class GenAIClient: def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to LLM with optional tool definitions. @@ -362,9 +401,9 @@ class GenAIClient: async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Streaming counterpart to `chat_with_tools`. diff --git a/frigate/genai/manager.py b/frigate/genai/manager.py index a1325d3279..1301f1b7a1 100644 --- a/frigate/genai/manager.py +++ b/frigate/genai/manager.py @@ -6,7 +6,7 @@ no chat feature is active) are never initialized. """ import logging -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from frigate.config import FrigateConfig from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum @@ -23,7 +23,7 @@ class GenAIClientManager: def __init__(self, config: FrigateConfig) -> None: self._configs: dict[str, GenAIConfig] = {} self._role_map: dict[GenAIRoleEnum, str] = {} - self._clients: dict[str, "GenAIClient"] = {} + self._clients: dict[str, GenAIClient] = {} self.update_config(config) def update_config(self, config: FrigateConfig) -> None: @@ -59,10 +59,12 @@ class GenAIClientManager: for role in genai_cfg.roles: self._role_map[role] = name - def _get_client(self, name: str) -> "Optional[GenAIClient]": + def _get_client(self, name: str) -> "GenAIClient | None": """Return the client for *name*, creating it on first access.""" if name in self._clients: - return self._clients[name] + client = self._clients[name] + client.ensure_provider() + return client from frigate.genai import PROVIDERS @@ -78,7 +80,7 @@ class GenAIClientManager: return None try: - client: "GenAIClient" = provider_cls(genai_cfg) + client = provider_cls(genai_cfg) except Exception as e: logger.exception( "Failed to create GenAI client for provider %s: %s", @@ -91,19 +93,19 @@ class GenAIClientManager: return client @property - def chat_client(self) -> "Optional[GenAIClient]": + def chat_client(self) -> "GenAIClient | None": """Client configured for the chat role (e.g. chat with function calling).""" name = self._role_map.get(GenAIRoleEnum.chat) return self._get_client(name) if name else None @property - def description_client(self) -> "Optional[GenAIClient]": + def description_client(self) -> "GenAIClient | None": """Client configured for the descriptions role (e.g. review descriptions, object descriptions).""" name = self._role_map.get(GenAIRoleEnum.descriptions) return self._get_client(name) if name else None @property - def embeddings_client(self) -> "Optional[GenAIClient]": + def embeddings_client(self) -> "GenAIClient | None": """Client configured for the embeddings role.""" name = self._role_map.get(GenAIRoleEnum.embeddings) return self._get_client(name) if name else None @@ -119,5 +121,6 @@ class GenAIClientManager: "models": client.list_models(), "roles": [r.value for r in genai_cfg.roles], "supports_toggleable_thinking": client.supports_toggleable_thinking, + "supports_embeddings": client.supports_embeddings, } return result diff --git a/frigate/genai/plugins/gemini.py b/frigate/genai/plugins/gemini.py index 9efd241893..1c89806082 100644 --- a/frigate/genai/plugins/gemini.py +++ b/frigate/genai/plugins/gemini.py @@ -4,7 +4,8 @@ import base64 import binascii import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from google import genai from google.genai import errors, types @@ -16,7 +17,7 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) -def _decode_thought_signature(value: Any) -> Optional[bytes]: +def _decode_thought_signature(value: Any) -> bytes | None: """Decode a base64-encoded thought_signature carried across conversation turns.""" if not value: return None @@ -30,14 +31,45 @@ def _decode_thought_signature(value: Any) -> Optional[bytes]: return None -def _encode_thought_signature(signature: Optional[bytes]) -> Optional[str]: +def _encode_thought_signature(signature: bytes | None) -> str | None: """Encode bytes thought_signature as base64 so it survives JSON-friendly transport.""" if not signature: return None return base64.b64encode(signature).decode("ascii") -def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]: +def _decode_data_uri(url: str) -> tuple[str, bytes] | None: + """Decode a ``data:`` URI into ``(mime_type, bytes)``; None if not a data URI.""" + if not isinstance(url, str) or not url.startswith("data:"): + return None + try: + header, b64 = url.split(",", 1) + mime = header[len("data:") :].split(";")[0] or "image/jpeg" + return mime, base64.b64decode(b64) + except (ValueError, binascii.Error): + return None + + +def _parts_from_content(content: Any) -> list[types.Part]: + """Convert OpenAI-style message content (str or multimodal list) to Gemini parts.""" + if isinstance(content, list): + parts: list[types.Part] = [] + for item in content: + if not isinstance(item, dict): + continue + if item.get("type") == "text": + parts.append(types.Part.from_text(text=item.get("text") or "")) + elif item.get("type") == "image_url": + decoded = _decode_data_uri((item.get("image_url") or {}).get("url", "")) + if decoded is not None: + mime, data = decoded + parts.append(types.Part.from_bytes(data=data, mime_type=mime)) + # Gemini rejects empty parts; fall back to a single space. + return parts or [types.Part.from_text(text=" ")] + return [types.Part.from_text(text=content or "")] + + +def _stats_from_gemini_usage(usage: Any) -> dict[str, Any] | None: """Build a stats dict from a Gemini usage_metadata object.""" prompt_tokens = getattr(usage, "prompt_token_count", None) completion_tokens = getattr(usage, "candidates_token_count", None) @@ -84,9 +116,9 @@ class GeminiClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to Gemini.""" contents = [prompt] + [ types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images @@ -141,9 +173,9 @@ class GeminiClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to Gemini with optional tool definitions. @@ -226,9 +258,7 @@ class GeminiClient(GenAIClient): ) else: # user gemini_messages.append( - types.Content( - role="user", parts=[types.Part.from_text(text=content)] - ) + types.Content(role="user", parts=_parts_from_content(content)) ) # Convert tools to Gemini format @@ -399,9 +429,9 @@ class GeminiClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. @@ -484,9 +514,7 @@ class GeminiClient(GenAIClient): ) else: # user gemini_messages.append( - types.Content( - role="user", parts=[types.Part.from_text(text=content)] - ) + types.Content(role="user", parts=_parts_from_content(content)) ) # Convert tools to Gemini format @@ -552,9 +580,9 @@ class GeminiClient(GenAIClient): # Use streaming API content_parts: list[str] = [] reasoning_parts: list[str] = [] - tool_calls_by_index: dict[int, dict[str, Any]] = {} + tool_calls_accum: list[dict[str, Any]] = [] finish_reason = "stop" - usage_stats: Optional[dict[str, Any]] = None + usage_stats: dict[str, Any] | None = None stream = await self.provider.aio.models.generate_content_stream( model=self.genai_config.model, @@ -599,7 +627,11 @@ class GeminiClient(GenAIClient): content_parts.append(part.text) yield ("content_delta", part.text) elif part.function_call: - # Handle function call + # Gemini streams complete function calls (not partial + # argument deltas), so each part is a distinct tool + # call. Append rather than accumulate by name — the + # latter concatenated parallel/repeated calls into one + # invalid arguments string (e.g. `{...}{...}`). try: arguments = ( dict(part.function_call.args) @@ -609,40 +641,16 @@ class GeminiClient(GenAIClient): except Exception: arguments = {} - # Store tool call - tool_call_id = part.function_call.name or "" - tool_call_name = part.function_call.name or "" - - # Check if we already have this tool call - found_index = None - for idx, tc in tool_calls_by_index.items(): - if tc["name"] == tool_call_name: - found_index = idx - break - - if found_index is None: - found_index = len(tool_calls_by_index) - tool_calls_by_index[found_index] = { - "id": tool_call_id, - "name": tool_call_name, - "arguments": "", - "thought_signature": None, + tool_calls_accum.append( + { + "id": part.function_call.name or "", + "name": part.function_call.name or "", + "arguments": arguments, + "thought_signature": getattr( + part, "thought_signature", None + ), } - - # Accumulate arguments - if arguments: - tool_calls_by_index[found_index]["arguments"] += ( - json.dumps(arguments) - if isinstance(arguments, dict) - else str(arguments) - ) - - # Capture latest thought_signature for this call - chunk_sig = getattr(part, "thought_signature", None) - if chunk_sig: - tool_calls_by_index[found_index][ - "thought_signature" - ] = chunk_sig + ) # Build final message full_content = "".join(content_parts).strip() or None @@ -650,25 +658,20 @@ class GeminiClient(GenAIClient): # Convert tool calls to list format tool_calls_list = None - if tool_calls_by_index: - tool_calls_list = [] - for tc in tool_calls_by_index.values(): - try: - # Try to parse accumulated arguments as JSON - parsed_args = json.loads(tc["arguments"]) - except (json.JSONDecodeError, Exception): - parsed_args = tc["arguments"] - - tool_calls_list.append( - { - "id": tc["id"], - "name": tc["name"], - "arguments": parsed_args, - "thought_signature": _encode_thought_signature( - tc.get("thought_signature") - ), - } - ) + if tool_calls_accum: + tool_calls_list = [ + { + "id": tc["id"], + "name": tc["name"], + "arguments": tc["arguments"] + if isinstance(tc["arguments"], dict) + else {}, + "thought_signature": _encode_thought_signature( + tc.get("thought_signature") + ), + } + for tc in tool_calls_accum + ] finish_reason = "tool_calls" if usage_stats is not None: diff --git a/frigate/genai/plugins/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py index d5458cf8f9..af3ecc9b18 100644 --- a/frigate/genai/plugins/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -4,7 +4,8 @@ import base64 import io import json import logging -from typing import Any, AsyncGenerator, Optional, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import numpy as np @@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message logger = logging.getLogger(__name__) -def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> Optional[dict[str, Any]]: +def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> dict[str, Any] | None: """Build a stats dict from a llama.cpp streaming chunk. Final-chunk `usage` carries authoritative token counts. Per-chunk @@ -75,29 +76,6 @@ def _parse_launch_arg(args: list[str], flag: str) -> str | None: return args[idx + 1] -def _fetch_llama_props(base_url: str, model: str) -> dict[str, Any]: - """Fetch /props from a llama.cpp server, with llama-swap fallback. - - Raises the underlying RequestException if both endpoints fail; callers - decide how to surface the failure. - """ - try: - response = requests.get( - f"{base_url}/props", - params={"model": model}, - timeout=10, - ) - response.raise_for_status() - return cast(dict[str, Any], response.json()) - except Exception: - response = requests.get( - f"{base_url}/upstream/{model}/props", - timeout=10, - ) - response.raise_for_status() - return cast(dict[str, Any], response.json()) - - def _to_jpeg(img_bytes: bytes) -> bytes | None: """Convert image bytes to JPEG. llama.cpp/STB does not support WebP.""" try: @@ -127,6 +105,48 @@ class LlamaCppClient(GenAIClient): _text_baseline_tokens: int | None _media_marker: str + @property + def supports_embeddings(self) -> bool: + """llama.cpp exposes an /embeddings endpoint for any loaded model.""" + return True + + def _auth_headers(self) -> dict | None: + """Bearer auth header when an API key is configured, else None.""" + if self.genai_config.api_key: + return {"Authorization": "Bearer " + self.genai_config.api_key} + + return None + + def _get(self, url: str, **kwargs: Any) -> requests.Response: + """GET with the configured auth headers injected.""" + return requests.get(url, headers=self._auth_headers(), **kwargs) + + def _post(self, url: str, **kwargs: Any) -> requests.Response: + """POST with the configured auth headers injected.""" + return requests.post(url, headers=self._auth_headers(), **kwargs) + + def _fetch_llama_props(self, base_url: str, model: str) -> dict[str, Any]: + """Fetch /props from a llama.cpp server, with llama-swap fallback. + + Raises the underlying RequestException if both endpoints fail; callers + decide how to surface the failure. + """ + try: + response = self._get( + f"{base_url}/props", + params={"model": model}, + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + except Exception: + response = self._get( + f"{base_url}/upstream/{model}/props", + timeout=10, + ) + response.raise_for_status() + return cast(dict[str, Any], response.json()) + def _init_provider(self) -> str | None: """Initialize the client and query model metadata from the server.""" self.provider_options = { @@ -210,7 +230,7 @@ class LlamaCppClient(GenAIClient): model_entry: dict[str, Any] | None = None try: - response = requests.get(f"{base_url}/v1/models", timeout=10) + response = self._get(f"{base_url}/v1/models", timeout=10) response.raise_for_status() models_data = response.json() @@ -271,7 +291,7 @@ class LlamaCppClient(GenAIClient): info["supports_tools"] = True try: - props = _fetch_llama_props(base_url, configured_model) + props = self._fetch_llama_props(base_url, configured_model) if info["context_size"] is None: default_settings = props.get("default_generation_settings", {}) @@ -311,9 +331,9 @@ class LlamaCppClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to llama.cpp server.""" if self.provider is None: logger.warning( @@ -357,7 +377,7 @@ class LlamaCppClient(GenAIClient): if self.supports_toggleable_thinking: payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} - response = requests.post( + response = self._post( f"{self.provider}/v1/chat/completions", json=payload, timeout=self.timeout, @@ -407,7 +427,7 @@ class LlamaCppClient(GenAIClient): if base_url is None: return [] try: - response = requests.get(f"{base_url}/v1/models", timeout=10) + response = self._get(f"{base_url}/v1/models", timeout=10) response.raise_for_status() models = [] for m in response.json().get("data", []): @@ -510,7 +530,7 @@ class LlamaCppClient(GenAIClient): "messages": [{"role": "user", "content": content}], "max_tokens": 1, } - response = requests.post( + response = self._post( f"{self.provider}/v1/chat/completions", json=payload, timeout=60, @@ -521,10 +541,10 @@ class LlamaCppClient(GenAIClient): def _build_payload( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]], - tool_choice: Optional[str], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, stream: bool = False, - enable_thinking: Optional[bool] = None, + enable_thinking: bool | None = None, ) -> dict[str, Any]: """Build request payload for chat completions (sync or stream).""" openai_tool_choice = None @@ -588,7 +608,7 @@ class LlamaCppClient(GenAIClient): @staticmethod def _streamed_tool_calls_to_list( tool_calls_by_index: dict[int, dict[str, Any]], - ) -> Optional[list[dict[str, Any]]]: + ) -> list[dict[str, Any]] | None: """Convert streamed tool_calls index map to list of {id, name, arguments}.""" if not tool_calls_by_index: return None @@ -620,7 +640,7 @@ class LlamaCppClient(GenAIClient): if self.provider is None: return False try: - props = _fetch_llama_props(self.provider, self.genai_config.model) + props = self._fetch_llama_props(self.provider, self.genai_config.model) except Exception as e: logger.warning("Failed to refresh llama.cpp media marker: %s", e) return False @@ -681,7 +701,7 @@ class LlamaCppClient(GenAIClient): return content def post_embeddings() -> requests.Response: - return requests.post( + return self._post( f"{self.provider}/embeddings", json={"model": self.genai_config.model, "content": build_content()}, timeout=self.timeout, @@ -758,9 +778,9 @@ class LlamaCppClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to llama.cpp server with optional tool definitions. @@ -785,7 +805,7 @@ class LlamaCppClient(GenAIClient): stream=False, enable_thinking=enable_thinking, ) - response = requests.post( + response = self._post( f"{self.provider}/v1/chat/completions", json=payload, timeout=self.timeout, @@ -830,9 +850,9 @@ class LlamaCppClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools via OpenAI-compatible streaming API.""" if self.provider is None: @@ -866,6 +886,7 @@ class LlamaCppClient(GenAIClient): "POST", f"{self.provider}/v1/chat/completions", json=payload, + headers=self._auth_headers(), ) as response: response.raise_for_status() async for line in response.aiter_lines(): diff --git a/frigate/genai/plugins/ollama.py b/frigate/genai/plugins/ollama.py index 08176f524b..f323b0faa7 100644 --- a/frigate/genai/plugins/ollama.py +++ b/frigate/genai/plugins/ollama.py @@ -4,7 +4,8 @@ import base64 import binascii import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from httpx import RemoteProtocolError, TimeoutException from ollama import AsyncClient as OllamaAsyncClient @@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message logger = logging.getLogger(__name__) -def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]: +def _extract_ollama_stats(response: Any) -> dict[str, Any] | None: """Build a stats dict from Ollama's response metadata. Ollama reports eval_count/eval_duration (generation) and @@ -51,7 +52,7 @@ def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]: def _normalize_multimodal_content( content: Any, -) -> tuple[Optional[str], Optional[list[bytes]]]: +) -> tuple[str | None, list[bytes] | None]: """Convert OpenAI-style multimodal content to Ollama's (text, images) shape. The chat API constructs user messages with content as a list of @@ -98,7 +99,7 @@ class OllamaClient(GenAIClient): provider: ApiClient | None provider_options: dict[str, Any] - _supports_thinking_cache: Optional[bool] = None + _supports_thinking_cache: bool | None = None @property def supports_toggleable_thinking(self) -> bool: @@ -193,9 +194,9 @@ class OllamaClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to Ollama""" if self.provider is None: logger.warning( @@ -290,10 +291,10 @@ class OllamaClient(GenAIClient): def _build_request_params( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]], - tool_choice: Optional[str], + tools: list[dict[str, Any]] | None, + tool_choice: str | None, stream: bool = False, - enable_thinking: Optional[bool] = None, + enable_thinking: bool | None = None, ) -> dict[str, Any]: """Build request_messages and params for chat (sync or stream).""" request_messages = [] @@ -385,9 +386,9 @@ class OllamaClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: if self.provider is None: logger.warning( @@ -426,9 +427,9 @@ class OllamaClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """Stream chat with tools; yields content deltas then final message. diff --git a/frigate/genai/plugins/openai.py b/frigate/genai/plugins/openai.py index 3e862f8fd5..e89ab93922 100644 --- a/frigate/genai/plugins/openai.py +++ b/frigate/genai/plugins/openai.py @@ -3,7 +3,8 @@ import base64 import json import logging -from typing import Any, AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Any from httpx import TimeoutException from openai import OpenAI @@ -14,7 +15,7 @@ from frigate.genai import GenAIClient, register_genai_provider logger = logging.getLogger(__name__) -def _stats_from_openai_usage(usage: Any) -> Optional[dict[str, Any]]: +def _stats_from_openai_usage(usage: Any) -> dict[str, Any] | None: """Build a stats dict from an OpenAI-compatible usage object.""" if usage is None: return None @@ -35,7 +36,7 @@ class OpenAIClient(GenAIClient): """Generative AI client for Frigate using OpenAI.""" provider: OpenAI - context_size: Optional[int] = None + context_size: int | None = None def _init_provider(self) -> OpenAI: """Initialize the client. @@ -60,9 +61,9 @@ class OpenAIClient(GenAIClient): self, prompt: str, images: list[bytes], - response_format: Optional[dict] = None, + response_format: dict | None = None, enable_thinking: bool = False, - ) -> Optional[str]: + ) -> str | None: """Submit a request to OpenAI.""" encoded_images = [base64.b64encode(image).decode("utf-8") for image in images] messages_content: list[dict] = [ @@ -186,9 +187,9 @@ class OpenAIClient(GenAIClient): def chat_with_tools( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> dict[str, Any]: """ Send chat messages to OpenAI with optional tool definitions. @@ -307,9 +308,9 @@ class OpenAIClient(GenAIClient): async def chat_with_tools_stream( self, messages: list[dict[str, Any]], - tools: Optional[list[dict[str, Any]]] = None, - tool_choice: Optional[str] = "auto", - enable_thinking: Optional[bool] = None, + tools: list[dict[str, Any]] | None = None, + tool_choice: str | None = "auto", + enable_thinking: bool | None = None, ) -> AsyncGenerator[tuple[str, Any], None]: """ Stream chat with tools; yields content deltas then final message. @@ -357,7 +358,7 @@ class OpenAIClient(GenAIClient): reasoning_parts: list[str] = [] tool_calls_by_index: dict[int, dict[str, Any]] = {} finish_reason = "stop" - usage_stats: Optional[dict[str, Any]] = None + usage_stats: dict[str, Any] | None = None stream = self.provider.chat.completions.create(**request_params) @@ -422,9 +423,18 @@ class OpenAIClient(GenAIClient): for tc in tool_calls_by_index.values(): try: # Parse accumulated arguments as JSON - parsed_args = json.loads(tc["arguments"]) - except (json.JSONDecodeError, Exception): - parsed_args = tc["arguments"] + parsed_args = json.loads(tc["arguments"] or "{}") + except (json.JSONDecodeError, ValueError): + logger.warning( + "Failed to parse streamed tool call arguments for %s", + tc["name"], + ) + parsed_args = {} + + # Downstream (ToolCall model) requires a dict; never leak a + # partial/invalid arguments string. + if not isinstance(parsed_args, dict): + parsed_args = {} tool_calls_list.append( { diff --git a/frigate/genai/prompts.py b/frigate/genai/prompts.py index af6ddab889..33045606eb 100644 --- a/frigate/genai/prompts.py +++ b/frigate/genai/prompts.py @@ -6,7 +6,7 @@ transport. """ import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Literal from playhouse.shortcuts import model_to_dict @@ -216,7 +216,7 @@ def build_object_description_prompt( return template.format(**model_to_dict(event)) -def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]: +def get_attribute_classifications(config: FrigateConfig) -> list[dict[str, Any]]: """Return enabled custom classification models of `attribute` type. Each entry: {"name": , "objects": [, ...]}. @@ -224,7 +224,7 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]] types, which can later be filtered via the search_objects `attribute` field. """ - result: List[Dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for model_key, model_config in config.classification.custom.items(): if not model_config.enabled or model_config.object_config is None: @@ -248,8 +248,9 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]] def get_tool_definitions( semantic_search_enabled: bool = False, - attribute_classifications: Optional[List[Dict[str, Any]]] = None, -) -> List[Dict[str, Any]]: + attribute_classifications: list[dict[str, Any]] | None = None, + embeddings_language: Literal["english", "multi"] = "multi", +) -> list[dict[str, Any]]: """ Get OpenAI-compatible tool definitions for Frigate. @@ -258,9 +259,11 @@ def get_tool_definitions( tool exposes an additional `semantic_query` parameter for descriptive queries (e.g. "person riding a lawn mower") and find_similar_objects is included. When attribute classification models are configured, an - `attribute` parameter is exposed for filtering by their labels. + `attribute` parameter is exposed for filtering by their labels. When the + embeddings model only understands English (JinaV1), the `semantic_query` + description instructs the model to write the query in English. """ - search_objects_properties: Dict[str, Any] = { + search_objects_properties: dict[str, Any] = { "camera": { "type": "string", "description": "Camera name to filter by (optional).", @@ -349,6 +352,14 @@ def get_tool_definitions( "When set, combine with label/time/camera/zone filters as " "usual (e.g. label='person', semantic_query='riding a lawn " "mower', after='2024-05-01T00:00:00Z')." + + ( + " The configured embeddings model only understands " + "English, so always write semantic_query in English, " + "translating the user's description if they phrased it " + "in another language." + if embeddings_language == "english" + else "" + ) ), } @@ -657,9 +668,9 @@ def get_tool_definitions( def build_chat_system_prompt( config: FrigateConfig, - allowed_cameras: List[str], + allowed_cameras: list[str], semantic_search_enabled: bool, - attribute_classifications: List[Dict[str, Any]], + attribute_classifications: list[dict[str, Any]], ) -> str: """Build the system prompt for the chat completion endpoint. @@ -671,7 +682,7 @@ def build_chat_system_prompt( current_date_str = current_datetime.strftime("%Y-%m-%d") current_time_str = current_datetime.strftime("%I:%M:%S %p") - cameras_info: List[str] = [] + cameras_info: list[str] = [] has_speed_zone = False for camera_id in allowed_cameras: if camera_id not in config.cameras: @@ -682,14 +693,17 @@ def build_chat_system_prompt( if camera_config.friendly_name else camera_id.replace("_", " ").title() ) - zone_names = list(camera_config.zones.keys()) + zone_descriptors = [ + f"{zone_config.get_formatted_name(zone_name)} (ID: {zone_name})" + for zone_name, zone_config in camera_config.zones.items() + ] if not has_speed_zone: has_speed_zone = any( zone.distances for zone in camera_config.zones.values() ) - if zone_names: + if zone_descriptors: cameras_info.append( - f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_names)})" + f" - {friendly_name} (ID: {camera_id}, zones: {', '.join(zone_descriptors)})" ) else: cameras_info.append(f" - {friendly_name} (ID: {camera_id})") @@ -699,7 +713,7 @@ def build_chat_system_prompt( cameras_section = ( "\n\nAvailable cameras:\n" + "\n".join(cameras_info) - + "\n\nWhen users refer to cameras by their friendly name (e.g., 'Back Deck Camera'), use the corresponding camera ID (e.g., 'back_deck_cam') in tool calls." + + "\n\nWhen users refer to cameras or zones by their friendly name (e.g., 'Back Deck Camera', 'Front Walkway'), use the corresponding ID (e.g., 'back_deck_cam', 'front_walk') in tool calls. Tool results also identify zones by their ID, so when presenting cameras or zones back to the user, translate the ID to its friendly name." ) speed_units_section = "" diff --git a/frigate/genai/utils.py b/frigate/genai/utils.py index a382647cb9..fbd6f2110c 100644 --- a/frigate/genai/utils.py +++ b/frigate/genai/utils.py @@ -2,14 +2,14 @@ import json import logging -from typing import Any, List, Optional +from typing import Any logger = logging.getLogger(__name__) def parse_tool_calls_from_message( message: dict[str, Any], -) -> Optional[list[dict[str, Any]]]: +) -> list[dict[str, Any]] | None: """ Parse tool_calls from an OpenAI-style message dict. @@ -52,7 +52,7 @@ def parse_tool_calls_from_message( def build_assistant_message_for_conversation( content: Any, - tool_calls_raw: Optional[List[dict[str, Any]]], + tool_calls_raw: list[dict[str, Any]] | None, ) -> dict[str, Any]: """ Build the assistant message dict in OpenAI format for appending to a conversation. diff --git a/frigate/jobs/debug_replay.py b/frigate/jobs/debug_replay.py index 3d8b2d6b63..c5e4ed8761 100644 --- a/frigate/jobs/debug_replay.py +++ b/frigate/jobs/debug_replay.py @@ -66,7 +66,7 @@ class DebugReplayJob(Job): replay_camera_name: str = "" start_ts: float = 0.0 end_ts: float = 0.0 - current_step: Optional[str] = None + current_step: str | None = None progress_percent: float = 0.0 def to_dict(self) -> dict[str, Any]: @@ -255,7 +255,7 @@ class DebugReplayJobRunner(threading.Thread): frigate_config: FrigateConfig, config_publisher: CameraConfigUpdatePublisher, replay_manager: "DebugReplayManager", - publisher: Optional[JobStatePublisher] = None, + publisher: JobStatePublisher | None = None, ) -> None: super().__init__(daemon=True, name=f"debug_replay_{job.id}") self.job = job diff --git a/frigate/jobs/export.py b/frigate/jobs/export.py index a74b91713e..1c88ec5849 100644 --- a/frigate/jobs/export.py +++ b/frigate/jobs/export.py @@ -4,15 +4,17 @@ import logging import os import threading import time +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from queue import Full, Queue -from typing import Any, Callable, Optional +from typing import Any 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 @@ -46,15 +48,16 @@ class ExportJob(Job): job_type: str = "export" camera: str = "" - name: Optional[str] = None - image_path: Optional[str] = None - export_case_id: Optional[str] = None + name: str | None = None + image_path: str | None = None + export_case_id: str | None = None request_start_time: float = 0.0 request_end_time: float = 0.0 playback_source: str = PlaybackSourceEnum.recordings.value - ffmpeg_input_args: Optional[str] = None - ffmpeg_output_args: Optional[str] = None + ffmpeg_input_args: str | None = None + ffmpeg_output_args: str | None = None cpu_fallback: bool = False + chapters: ChaptersEnum | None = None current_step: str = "queued" progress_percent: float = 0.0 @@ -147,7 +150,7 @@ class ExportJobManager: config: FrigateConfig, max_concurrent: int, max_queued: int = MAX_QUEUED_EXPORT_JOBS, - publisher: Optional[JobStatePublisher] = None, + publisher: JobStatePublisher | None = None, ) -> None: self.config = config self.max_concurrent = max(1, max_concurrent) @@ -266,7 +269,7 @@ class ExportJobManager: return job.id - def get_job(self, job_id: str) -> Optional[ExportJob]: + def get_job(self, job_id: str) -> ExportJob | None: """Get a job by ID.""" with self.lock: return self.jobs.get(job_id) @@ -343,6 +346,7 @@ class ExportJobManager: job.ffmpeg_input_args, job.ffmpeg_output_args, job.cpu_fallback, + job.chapters, on_progress=self._make_progress_callback(job), ) @@ -376,7 +380,7 @@ class ExportJobManager: self._schedule_job_cleanup(job.id) -_job_manager: Optional[ExportJobManager] = None +_job_manager: ExportJobManager | None = None _job_manager_lock = threading.Lock() @@ -482,7 +486,7 @@ def start_export_job(config: FrigateConfig, job: ExportJob) -> str: return get_export_job_manager(config).enqueue(job) -def get_export_job(config: FrigateConfig, job_id: str) -> Optional[ExportJob]: +def get_export_job(config: FrigateConfig, job_id: str) -> ExportJob | None: """Get a queued or completed export job by ID.""" return get_export_job_manager(config).get_job(job_id) diff --git a/frigate/jobs/job.py b/frigate/jobs/job.py index a445eebf53..c40087d0cd 100644 --- a/frigate/jobs/job.py +++ b/frigate/jobs/job.py @@ -1,7 +1,7 @@ """Generic base class for long-running background jobs.""" from dataclasses import asdict, dataclass, field -from typing import Any, Optional +from typing import Any @dataclass @@ -11,10 +11,10 @@ class Job: id: str = field(default_factory=lambda: __import__("uuid").uuid4().__str__()[:12]) job_type: str = "" # Must be set by subclasses status: str = "queued" # queued, running, success, failed, cancelled - results: Optional[dict[str, Any]] = None - start_time: Optional[float] = None - end_time: Optional[float] = None - error_message: Optional[str] = None + results: dict[str, Any] | None = None + start_time: float | None = None + end_time: float | None = None + error_message: str | None = None def to_dict(self) -> dict[str, Any]: """Convert to dictionary for WebSocket transmission.""" diff --git a/frigate/jobs/manager.py b/frigate/jobs/manager.py index 8aa77b3c7a..c2fb44af38 100644 --- a/frigate/jobs/manager.py +++ b/frigate/jobs/manager.py @@ -1,14 +1,13 @@ """Generic job management for long-running background tasks.""" import threading -from typing import Optional from frigate.jobs.job import Job from frigate.types import JobStatusTypesEnum # Global state and locks for enforcing single concurrent job per job type _job_locks: dict[str, threading.Lock] = {} -_current_jobs: dict[str, Optional[Job]] = {} +_current_jobs: dict[str, Job | None] = {} # Keep completed jobs for retrieval, keyed by (job_type, job_id) _completed_jobs: dict[tuple[str, str], Job] = {} @@ -35,7 +34,7 @@ def set_current_job(job: Job) -> None: _current_jobs[job.job_type] = job -def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None: +def clear_current_job(job_type: str, job_id: str | None = None) -> None: """Clear the current job for a given job type, optionally checking the ID.""" lock = _get_lock(job_type) with lock: @@ -45,14 +44,14 @@ def clear_current_job(job_type: str, job_id: Optional[str] = None) -> None: _current_jobs[job_type] = None -def get_current_job(job_type: str) -> Optional[Job]: +def get_current_job(job_type: str) -> Job | None: """Get the current running/queued job for a given job type, if any.""" lock = _get_lock(job_type) with lock: return _current_jobs.get(job_type) -def get_job_by_id(job_type: str, job_id: str) -> Optional[Job]: +def get_job_by_id(job_type: str, job_id: str) -> Job | None: """Get job by ID. Checks current job first, then completed jobs.""" lock = _get_lock(job_type) with lock: diff --git a/frigate/jobs/media_sync.py b/frigate/jobs/media_sync.py index 4a3fdc3557..1cd8209869 100644 --- a/frigate/jobs/media_sync.py +++ b/frigate/jobs/media_sync.py @@ -5,7 +5,7 @@ import os import threading from dataclasses import dataclass, field from datetime import datetime -from typing import Optional, cast +from typing import cast from frigate.comms.inter_process import InterProcessRequestor from frigate.const import CONFIG_DIR, UPDATE_JOB_STATE @@ -87,7 +87,7 @@ class MediaSyncRunner(threading.Thread): self._broadcast_status() except Exception as e: - logger.error(f"Media sync job {self.job.id} failed: {e}", exc_info=True) + logger.exception(f"Media sync job {self.job.id} failed: {e}") self.job.status = JobStatusTypesEnum.failed self.job.error_message = str(e) self.job.end_time = datetime.now().timestamp() @@ -110,10 +110,10 @@ class MediaSyncRunner(threading.Thread): def start_media_sync_job( dry_run: bool = False, - media_types: Optional[list[str]] = None, + media_types: list[str] | None = None, force: bool = False, verbose: bool = False, -) -> Optional[str]: +) -> str | None: """Start a new media sync job if none is currently running. Returns job ID on success, None if job already running. @@ -144,11 +144,11 @@ def start_media_sync_job( return job.id -def get_current_media_sync_job() -> Optional[MediaSyncJob]: +def get_current_media_sync_job() -> MediaSyncJob | None: """Get the current running/queued media sync job, if any.""" - return cast(Optional[MediaSyncJob], get_current_job("media_sync")) + return cast(MediaSyncJob | None, get_current_job("media_sync")) -def get_media_sync_job_by_id(job_id: str) -> Optional[MediaSyncJob]: +def get_media_sync_job_by_id(job_id: str) -> MediaSyncJob | None: """Get media sync job by ID. Currently only tracks the current job.""" - return cast(Optional[MediaSyncJob], get_job_by_id("media_sync", job_id)) + return cast(MediaSyncJob | None, get_job_by_id("media_sync", job_id)) diff --git a/frigate/jobs/motion_search.py b/frigate/jobs/motion_search.py index 13fc841e99..15cd104f7b 100644 --- a/frigate/jobs/motion_search.py +++ b/frigate/jobs/motion_search.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Generator, Iterable from concurrent.futures import Future, ThreadPoolExecutor, as_completed from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional, cast +from typing import Any, cast import cv2 import numpy as np @@ -102,11 +102,11 @@ class MotionSearchJob(Job): total_frames_processed: int = 0 # Live progress (ride the existing to_dict() websocket broadcast) - scanning_timestamp: Optional[float] = None + scanning_timestamp: float | None = None progress: float = 0.0 # Metrics for observability - metrics: Optional[MotionSearchMetrics] = None + metrics: MotionSearchMetrics | None = None def to_dict(self) -> dict[str, Any]: """Convert to dictionary for WebSocket transmission.""" @@ -911,14 +911,14 @@ def start_motion_search_job( return job.id -def get_motion_search_job(job_id: str) -> Optional[MotionSearchJob]: +def get_motion_search_job(job_id: str) -> MotionSearchJob | None: """Get a motion search job by ID.""" with _jobs_lock: job_entry = _motion_search_jobs.get(job_id) if job_entry: return job_entry[0] # Check completed jobs via manager - return cast(Optional[MotionSearchJob], get_job_by_id("motion_search", job_id)) + return cast(MotionSearchJob | None, get_job_by_id("motion_search", job_id)) def cancel_motion_search_job(job_id: str) -> bool: @@ -942,7 +942,7 @@ def cancel_motion_search_job(job_id: str) -> bool: job_payload = job.to_dict() logger.info("Cancelled motion search job %s", job_id) - requestor: Optional[InterProcessRequestor] = None + requestor: InterProcessRequestor | None = None try: requestor = InterProcessRequestor() requestor.send_data(UPDATE_JOB_STATE, job_payload) diff --git a/frigate/jobs/vlm_watch.py b/frigate/jobs/vlm_watch.py index 41ed830f14..1e6a542c21 100644 --- a/frigate/jobs/vlm_watch.py +++ b/frigate/jobs/vlm_watch.py @@ -8,7 +8,7 @@ import threading import time from dataclasses import asdict, dataclass, field from datetime import datetime -from typing import Any, Optional +from typing import Any import cv2 @@ -360,8 +360,8 @@ class VLMWatchRunner(threading.Thread): # Module-level singleton (only one watch job at a time) -_current_job: Optional[VLMWatchJob] = None -_cancel_event: Optional[threading.Event] = None +_current_job: VLMWatchJob | None = None +_cancel_event: threading.Event | None = None _job_lock = threading.Lock() @@ -444,6 +444,6 @@ def stop_vlm_watch_job() -> bool: return True -def get_vlm_watch_job() -> Optional[VLMWatchJob]: +def get_vlm_watch_job() -> VLMWatchJob | None: """Return the current (or most recent) VLM watch job.""" return _current_job diff --git a/frigate/log.py b/frigate/log.py index cd475a4bdb..3c5f8ec8b8 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -6,13 +6,14 @@ import os import sys import threading from collections import deque +from collections.abc import Callable, Generator from contextlib import contextmanager from enum import Enum from functools import wraps from logging.handlers import QueueHandler, QueueListener from multiprocessing.managers import SyncManager from queue import Empty, Queue -from typing import Any, Callable, Deque, Generator, Optional +from typing import Any from frigate.util.builtin import clean_camera_user_pass @@ -47,8 +48,8 @@ class LogLevel(str, Enum): critical = "critical" -log_listener: Optional[QueueListener] = None -log_queue: Optional[Queue] = None +log_listener: QueueListener | None = None +log_queue: Queue | None = None def setup_logging(manager: SyncManager) -> None: @@ -118,7 +119,7 @@ class LogPipe(threading.Thread): super().__init__(daemon=False) self.logger = logging.getLogger(log_name) self.level = level - self.deque: Deque[str] = deque(maxlen=100) + self.deque: deque[str] = deque(maxlen=100) self.fdRead, self.fdWrite = os.pipe() self.pipeReader = os.fdopen(self.fdRead) self.start() diff --git a/frigate/motion/__init__.py b/frigate/motion/__init__.py index 58f781f465..e6ead35bb7 100644 --- a/frigate/motion/__init__.py +++ b/frigate/motion/__init__.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from typing import Tuple from numpy import ndarray @@ -10,7 +9,7 @@ class MotionDetector(ABC): @abstractmethod def __init__( self, - frame_shape: Tuple[int, int, int], + frame_shape: tuple[int, int, int], config: MotionConfig, fps: int, improve_contrast: bool, diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index 6694dafff5..598eeacd1d 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import cv2 import numpy as np @@ -19,7 +18,7 @@ class ImprovedMotionDetector(MotionDetector): frame_shape: tuple[int, ...], config: RuntimeMotionConfig, fps: int, - ptz_metrics: Optional[PTZMetrics] = None, + ptz_metrics: PTZMetrics | None = None, name: str = "improved", blur_radius: int = 1, interpolation: int = cv2.INTER_NEAREST, diff --git a/frigate/object_detection/base.py b/frigate/object_detection/base.py index f2336f3da8..bc7910e4d0 100644 --- a/frigate/object_detection/base.py +++ b/frigate/object_detection/base.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from collections import deque from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any import numpy as np import zmq @@ -42,9 +42,9 @@ class ObjectDetector(ABC): class BaseLocalDetector(ObjectDetector): def __init__( self, - detector_config: Optional[BaseDetectorConfig] = None, - labels: Optional[str] = None, - stop_event: Optional[MpEvent] = None, + detector_config: BaseDetectorConfig | None = None, + labels: str | None = None, + stop_event: MpEvent | None = None, ) -> None: self.fps = EventsPerSecond() if labels is None: diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 477e9a0db7..a38669cbf9 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -10,7 +10,7 @@ import subprocess as sp import threading import traceback from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -335,6 +335,7 @@ class BirdsEyeFrameManager: self.camera_layout: list[Any] = [] self.active_cameras: set[str] = set() + self.layout_camera_order: list[str] = [] self.last_output_time = 0.0 def add_camera(self, cam: str) -> None: @@ -372,6 +373,13 @@ class BirdsEyeFrameManager: if cam in self.cameras: del self.cameras[cam] + def sort_cameras(self, cameras: set[str]) -> list[str]: + """Sort cameras by birdseye order, falling back to name when tied.""" + return sorted( + cameras, + key=lambda camera: (self.config.cameras[camera].birdseye.order, camera), + ) + def clear_frame(self) -> None: logger.debug("Clearing the birdseye frame") self.frame[:] = self.blank_frame @@ -379,8 +387,8 @@ class BirdsEyeFrameManager: def copy_to_position( self, position: Any, - camera: Optional[str] = None, - frame: Optional[np.ndarray] = None, + camera: str | None = None, + frame: np.ndarray | None = None, ) -> None: if camera is None: frame = None @@ -428,7 +436,7 @@ class BirdsEyeFrameManager: } return coordinates - def update_frame(self, frame: Optional[np.ndarray] = None) -> tuple[bool, bool]: + def update_frame(self, frame: np.ndarray | None = None) -> tuple[bool, bool]: """ Update birdseye, optionally with a new frame. Returns (frame_changed, layout_changed) to indicate if the frame or layout changed. @@ -482,6 +490,7 @@ class BirdsEyeFrameManager: # if the layout needs to be cleared self.camera_layout = [] self.active_cameras = set() + self.layout_camera_order = [] self.clear_frame() frame_changed = True layout_changed = True @@ -500,21 +509,21 @@ class BirdsEyeFrameManager: else: reset_layout = True + sorted_active_cameras = self.sort_cameras(active_cameras) + + if not reset_layout and sorted_active_cameras != self.layout_camera_order: + logger.debug("Birdseye camera order changed") + reset_layout = True + if reset_layout: logger.debug("Resetting Birdseye layout...") self.clear_frame() self.active_cameras = active_cameras + self.layout_camera_order = sorted_active_cameras layout_changed = True # Layout is changing due to reset # this also converts added_cameras from a set to a list since we need # to pop elements in order - active_cameras_to_add = sorted( - active_cameras, - # sort cameras by order and by name if the order is the same - key=lambda active_camera: ( - self.config.cameras[active_camera].birdseye.order, - active_camera, - ), - ) + active_cameras_to_add = sorted_active_cameras if len(active_cameras) == 1: # show single camera as fullscreen camera = active_cameras_to_add[0] @@ -592,12 +601,12 @@ class BirdsEyeFrameManager: self, cameras_to_add: list[str], coefficient: float, - ) -> Optional[list[list[Any]]]: + ) -> list[list[Any]] | None: """Calculate the optimal layout for 2+ cameras.""" def map_layout( camera_layout: list[list[Any]], row_height: int - ) -> tuple[int, int, Optional[list[list[Any]]]]: + ) -> tuple[int, int, list[list[Any]] | None]: """Map the calculated layout.""" candidate_layout = [] starting_x = 0 @@ -780,6 +789,7 @@ class BirdsEyeFrameManager: frame_changed, layout_changed = False, False self.active_cameras = set() self.camera_layout = [] + self.layout_camera_order = [] print(traceback.format_exc()) # if the frame was updated or the fps is too low, send frame @@ -825,7 +835,7 @@ class Birdseye: self.stop_event = stop_event self.requestor = InterProcessRequestor() self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps - self._idle_interval: Optional[float] = ( + self._idle_interval: float | None = ( (1.0 / self.idle_fps) if self.idle_fps > 0 else None ) diff --git a/frigate/output/preview.py b/frigate/output/preview.py index bf3c4bc7ef..cce05565a0 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -251,7 +251,7 @@ class PreviewRecorder: # end segment at end of hour (use UTC to avoid DST issues) self.segment_end = ( - (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)) + (datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=1)) .replace(minute=0, second=0, microsecond=0) .timestamp() ) @@ -263,7 +263,7 @@ class PreviewRecorder: # check for existing items in cache start_ts = ( - datetime.datetime.now(datetime.timezone.utc) + datetime.datetime.now(datetime.UTC) .replace(minute=0, second=0, microsecond=0) .timestamp() ) @@ -298,7 +298,7 @@ class PreviewRecorder: def reset_frame_cache(self, frame_time: float) -> None: self.segment_end = ( ( - datetime.datetime.fromtimestamp(frame_time, tz=datetime.timezone.utc) + datetime.datetime.fromtimestamp(frame_time, tz=datetime.UTC) + datetime.timedelta(hours=1) ) .replace(minute=0, second=0, microsecond=0) diff --git a/frigate/plus.py b/frigate/plus.py index 2870d2ae5f..d528aa1757 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -4,7 +4,7 @@ import logging import os import re from pathlib import Path -from typing import Any, List +from typing import Any import cv2 import requests @@ -140,8 +140,8 @@ class PlusApi: def add_false_positive( self, plus_id: str, - region: List[float], - bbox: List[float], + region: list[float], + bbox: list[float], score: float, label: str, model_hash: str, @@ -184,7 +184,7 @@ class PlusApi: def add_annotation( self, plus_id: str, - bbox: List[float], + bbox: list[float], label: str, difficult: bool = False, ) -> None: diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index fb76f6718d..e9bbda9889 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -20,6 +20,10 @@ from norfair.camera_motion import ( from frigate.camera import PTZMetrics from frigate.comms.dispatcher import Dispatcher from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum +from frigate.config.camera.updater import ( + CameraConfigUpdateEnum, + CameraConfigUpdateSubscriber, +) from frigate.const import ( AUTOTRACKING_MAX_AREA_RATIO, AUTOTRACKING_MAX_MOVE_METRICS, @@ -48,6 +52,22 @@ def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time): ) +def transform_is_finite(coord_transformations) -> bool: + """Return True if a norfair coordinate transform contains only finite values. + + A near-singular homography (common when the motion estimator can't find + enough stable features during zoom on a low-texture scene) can produce + inf/nan matrix entries. norfair accumulates the homography across frames, so + a single bad transform poisons every subsequent one and propagates nan into + the tracker's distance function, crashing the camera process. + """ + for attr in ("homography_matrix", "inverse_homography_matrix", "movement_vector"): + value = getattr(coord_transformations, attr, None) + if value is not None and not np.all(np.isfinite(value)): + return False + return True + + class PtzMotionEstimator: def __init__(self, config: CameraConfig, ptz_metrics: PTZMetrics) -> None: self.frame_manager = SharedMemoryFrameManager() @@ -135,6 +155,19 @@ class PtzMotionEstimator: ) self.coord_transformations = None + # A degenerate homography can yield non-finite transform values that + # norfair would accumulate and feed to the tracker as nan estimates. + # Drop the bad transform and request a reset so the estimator rebuilds + # a fresh reference frame instead of poisoning every following frame. + if self.coord_transformations is not None and not transform_is_finite( + self.coord_transformations + ): + logger.warning( + f"Autotracker: motion estimator produced a non-finite transform for {camera} at frame time {frame_time}, resetting" + ) + self.coord_transformations = None + self.ptz_metrics.reset.set() + try: logger.debug( f"{camera}: Motion estimator transformation: {self.coord_transformations.rel_to_abs([[0, 0]])}" @@ -165,7 +198,9 @@ class PtzAutoTrackerThread(threading.Thread): def run(self): while not self.stop_event.wait(1): - for camera, camera_config in self.config.cameras.items(): + self.ptz_autotracker.check_for_updates() + + for camera, camera_config in list(self.config.cameras.items()): if not camera_config.enabled: continue @@ -182,6 +217,7 @@ class PtzAutoTrackerThread(threading.Thread): self.ptz_autotracker.tracked_object[camera] = None self.ptz_autotracker.tracked_object_history[camera].clear() + self.ptz_autotracker.config_subscriber.stop() logger.info("Exiting autotracker...") @@ -215,6 +251,16 @@ class PtzAutoTracker: self.zoom_time: dict[str, float] = {} self.zoom_factor: dict[str, object] = {} + self.config_subscriber = CameraConfigUpdateSubscriber( + self.config, + self.config.cameras, + [ + CameraConfigUpdateEnum.add, + CameraConfigUpdateEnum.autotracking, + CameraConfigUpdateEnum.onvif, + ], + ) + # if cam is set to autotrack, onvif should be set up for camera, camera_config in self.config.cameras.items(): if not camera_config.enabled: @@ -231,6 +277,29 @@ class PtzAutoTracker: # Wait for the coroutine to complete future.result() + def check_for_updates(self) -> None: + """Apply camera config updates and mirror autotracking state to ptz metrics. + + The camera processes read autotracker_enabled rather than the config, so it + has to follow every path that can change autotracking, not just the mqtt + toggle that writes it directly. + """ + updates = self.config_subscriber.check_for_updates() + + for cameras in updates.values(): + for camera in cameras: + camera_config = self.config.cameras.get(camera) + metrics = self.ptz_metrics.get(camera) + + # a camera added at runtime gets its metrics from the maintainer on + # another thread, which seeds them from this same config value + if camera_config is None or metrics is None: + continue + + metrics.autotracker_enabled.value = ( + camera_config.onvif.autotracking.enabled + ) + async def _autotracker_setup(self, camera_config: CameraConfig, camera: str): logger.debug(f"{camera}: Autotracker init") @@ -727,7 +796,7 @@ class PtzAutoTracker: try: # Asynchronously wait for move data with a timeout move_data = await asyncio.wait_for(move_queue.get(), timeout=0.1) - except asyncio.TimeoutError: + except TimeoutError: continue async with self.move_queue_locks[camera]: @@ -919,8 +988,8 @@ class PtzAutoTracker: if invalid: logger.debug( - f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: " - + ", ".join( + f"{camera}: Invalid velocity: {tuple(np.round(velocities, 2).flatten().astype(int))}: Invalid because: %s", + ", ".join( [ var_name for var_name, is_invalid in [ @@ -932,7 +1001,7 @@ class PtzAutoTracker: ] if is_invalid ] - ) + ), ) # invalid velocity return False, np.zeros((4,)) @@ -1336,7 +1405,7 @@ class PtzAutoTracker: camera_config = self.config.cameras[camera] if camera_config.onvif.autotracking.enabled: - if not self.autotracker_init[camera]: + if not self.autotracker_init.get(camera): future = asyncio.run_coroutine_threadsafe( self._autotracker_setup(camera_config, camera), self.onvif.loop ) @@ -1454,9 +1523,11 @@ class PtzAutoTracker: } async def camera_maintenance(self, camera): - # bail and don't check anything if we're calibrating or tracking an object + # bail and don't check anything if we're not set up yet, calibrating, or + # tracking an object. a camera enabled at runtime has no autotracker_init + # entry until autotrack_object sets it up if ( - not self.autotracker_init[camera] + not self.autotracker_init.get(camera) or self.calibrating[camera] or self.tracked_object[camera] is not None ): diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index e48b3e7877..3fa786c883 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -344,16 +344,17 @@ class OnvifController: autotracking_config.enabled_in_config and autotracking_config.enabled ) - # autotracking-only: status request and service capabilities - if autotracking_enabled: - status_request = ptz.create_type("GetStatus") - status_request.ProfileToken = profile.token - self.cams[camera_name]["status_request"] = status_request + # these are local and cost nothing to build, and autotracking can be enabled + # after a camera is initialized, so always create them rather than baking the + # current config value into init state + status_request = ptz.create_type("GetStatus") + status_request.ProfileToken = profile.token + self.cams[camera_name]["status_request"] = status_request - service_capabilities_request = ptz.create_type("GetServiceCapabilities") - self.cams[camera_name]["service_capabilities_request"] = ( - service_capabilities_request - ) + service_capabilities_request = ptz.create_type("GetServiceCapabilities") + self.cams[camera_name]["service_capabilities_request"] = ( + service_capabilities_request + ) # setup relative move request when FOV relative movement is supported if ( @@ -856,7 +857,7 @@ class OnvifController: try: # Wait with a timeout to prevent blocking indefinitely future.result(timeout=10) - except asyncio.TimeoutError: + except TimeoutError: logger.error(f"Command {command} timed out for camera {camera_name}") except Exception as e: logger.error( diff --git a/frigate/record/export.py b/frigate/record/export.py index e89742b1ab..5d307077c9 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -9,14 +9,15 @@ import shutil import string import subprocess as sp import threading +from collections.abc import Callable from enum import Enum from pathlib import Path -from typing import Callable, Optional 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, @@ -42,33 +43,118 @@ TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey" # Captures the floating-point factor so we can scale expected duration. SETPTS_FACTOR_RE = re.compile(r"setpts=([0-9]*\.?[0-9]+)\*PTS") -# ffmpeg flags that can read from or write to arbitrary files -BLOCKED_FFMPEG_ARGS = frozenset( +# Allowlisted flags that take no value. +_VALUELESS_FLAGS = frozenset({"-an", "-sn", "-dn"}) + +# Allowlisted filter flags. Their value is validated as a filtergraph and may +# only reference filters in _SAFE_FILTERS. +_FILTER_FLAGS = frozenset({"-vf", "-af", "-filter"}) + +# Allowlisted flags that take exactly one value (encoder / muxer-safe options). +_VALUE_FLAGS = frozenset( { - "-i", - "-filter_script", - "-filter_complex", - "-lavfi", - "-vf", - "-af", - "-filter", - "-vstats_file", - "-passlogfile", - "-sdp_file", - "-dump_attachment", - "-attach", + "-c", + "-codec", + "-b", + "-crf", + "-qp", + "-q", + "-qscale", + "-preset", + "-tune", + "-profile", + "-level", + "-pix_fmt", + "-r", + "-g", + "-keyint_min", + "-sc_threshold", + "-bf", + "-refs", + "-qmin", + "-qmax", + "-maxrate", + "-minrate", + "-bufsize", + "-movflags", + "-threads", + "-aspect", + "-fps_mode", + "-vsync", + "-skip_frame", } ) +_ALLOWED_FLAGS = _VALUELESS_FLAGS | _FILTER_FLAGS | _VALUE_FLAGS + +# Filters that cannot read files, load plugins, or open network sources. +_SAFE_FILTERS = frozenset( + { + "setpts", + "fps", + "scale", + "format", + "transpose", + "hflip", + "vflip", + "crop", + "pad", + "setsar", + "setdar", + } +) + +# Conservative shape for a non-filter flag value. Excludes "/" (paths / +# filtergraph division), whitespace, brackets, and a leading "-" so a value +# can never be a path or swallow a following flag. ":" is permitted for values +# like "16:9". +_SAFE_VALUE_RE = re.compile(r"^[A-Za-z0-9_.:+][A-Za-z0-9_.:+-]*$") + +# Substrings inside a filtergraph that indicate a file-reading filter option. +# "movie=" also matches "amovie=" as a substring. +_BLOCKED_FILTER_VALUE_MARKERS = ("movie=", "textfile=", "filename=", "fontfile=") + + +def _base_flag(token: str) -> str: + """Return a flag's base name, lowercased and without its stream specifier. + + e.g. "-c:v" -> "-c", "-filter:a:0" -> "-filter". + """ + return token.lower().split(":", 1)[0] + + +def _validate_filtergraph(value: str) -> tuple[bool, str]: + """Validate a filtergraph value, allowing only filters in _SAFE_FILTERS.""" + # None of the safe filters need any of these + if any(token in value for token in ("://", "..", "[", "]")): + return False, "Invalid filter graph in custom ffmpeg arguments" + + lowered = value.lower() + if any(marker in lowered for marker in _BLOCKED_FILTER_VALUE_MARKERS): + return False, "File-reading filters are not allowed in custom ffmpeg arguments" + + # Filters are separated by "," within a chain and ";" between chains. Safe + # filters never use unescaped "," or ";" in their arguments, so splitting on + # them to recover filter names cannot hide a disallowed filter. + for spec in re.split(r"[;,]", value): + spec = spec.strip() + if not spec: + continue + + name = spec.split("=", 1)[0].strip().lower() + if name not in _SAFE_FILTERS: + return False, f"Filter not allowed in custom ffmpeg arguments: {name}" + + return True, "" + def validate_ffmpeg_args(args: str) -> tuple[bool, str]: - """Validate that user-provided ffmpeg args don't allow input/output injection. + """Validate user-provided custom export ffmpeg args with an allowlist. - Blocks: - - The -i flag and other flags that read/write arbitrary files - - Filter flags (can read files via movie=/amovie= source filters) - - Absolute/relative file paths (potential extra outputs) - - URLs and ffmpeg protocol references (data exfiltration) + Every token must be an allowlisted flag or the value of one; filter values + may only reference safe filters; and no token may become a bare input or + output URL. This structurally prevents arbitrary file read/write, network + exfiltration/SSRF, and resource-exhaustion via the export endpoint. Admin users skip this validation entirely since they are trusted. """ @@ -76,26 +162,36 @@ def validate_ffmpeg_args(args: str) -> tuple[bool, str]: return True, "" tokens = args.split() - for token in tokens: - # Block flags that could inject inputs or write to arbitrary files - if token.lower() in BLOCKED_FFMPEG_ARGS: + i = 0 + while i < len(tokens): + token = tokens[i] + + # A bare (non-flag) token here would be parsed by ffmpeg as an input or + # output URL. Only the server sets inputs/outputs, never the user. + if not token.startswith("-"): + return False, f"Unexpected argument in custom ffmpeg arguments: {token}" + + base = _base_flag(token) + if base not in _ALLOWED_FLAGS: return False, f"Forbidden ffmpeg argument: {token}" - # Block tokens that look like file paths (potential output injection) - if ( - token.startswith("/") - or token.startswith("./") - or token.startswith("../") - or token.startswith("~") - ): - return False, "File paths are not allowed in custom ffmpeg arguments" + if base in _VALUELESS_FLAGS: + i += 1 + continue - # Block URLs and ffmpeg protocol references (e.g. http://, tcp://, pipe:, file:) - if "://" in token or token.startswith("pipe:") or token.startswith("file:"): - return ( - False, - "Protocol references are not allowed in custom ffmpeg arguments", - ) + # Remaining flags consume exactly one value. + if i + 1 >= len(tokens): + return False, f"Missing value for ffmpeg argument: {token}" + + value = tokens[i + 1] + if base in _FILTER_FLAGS: + valid, message = _validate_filtergraph(value) + if not valid: + return False, message + elif not _SAFE_VALUE_RE.match(value): + return False, f"Invalid value for {token}: {value}" + + i += 2 return True, "" @@ -113,16 +209,17 @@ class RecordingExporter(threading.Thread): config: FrigateConfig, id: str, camera: str, - name: Optional[str], - image: Optional[str], + name: str | None, + image: str | None, start_time: int, end_time: int, playback_source: PlaybackSourceEnum, - export_case_id: Optional[str] = None, - ffmpeg_input_args: Optional[str] = None, - ffmpeg_output_args: Optional[str] = None, + export_case_id: str | None = None, + ffmpeg_input_args: str | None = None, + ffmpeg_output_args: str | None = None, cpu_fallback: bool = False, - on_progress: Optional[Callable[[str, float], None]] = None, + chapters: ChaptersEnum | None = None, + on_progress: Callable[[str, float], None] | None = None, ) -> None: super().__init__() self.config = config @@ -137,6 +234,7 @@ class RecordingExporter(threading.Thread): self.ffmpeg_input_args = ffmpeg_input_args self.ffmpeg_output_args = ffmpeg_output_args self.cpu_fallback = cpu_fallback + self.chapters = chapters self.on_progress = on_progress # ensure export thumb dir @@ -185,7 +283,7 @@ class RecordingExporter(threading.Thread): return input_duration * factor - def _sum_source_duration_seconds(self) -> Optional[float]: + def _sum_source_duration_seconds(self) -> float | None: """Sum saved-video seconds inside [start_time, end_time]. Queries Recordings or Previews depending on the playback source, @@ -285,7 +383,7 @@ class RecordingExporter(threading.Thread): def _chapter_metadata_path(self) -> str: return os.path.join(CACHE_DIR, f"export_chapters_{self.export_id}.txt") - def _build_chapter_metadata_file(self, recordings: list) -> Optional[str]: + def _build_chapter_metadata_file(self, recordings: list) -> str | None: """Write an FFmpeg metadata file with chapters for review items in range. Chapter offsets are computed in *output time*: the VOD endpoint @@ -414,6 +512,74 @@ class RecordingExporter(threading.Thread): return meta_path + def _build_recording_segment_chapter_metadata_file( + self, recordings: list + ) -> str | None: + """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: datetime.tzinfo | None = None + if tz_name: + try: + tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + tz = None + if tz is None: + tz = datetime.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") @@ -425,7 +591,7 @@ class RecordingExporter(threading.Thread): if ( self.start_time - < datetime.datetime.now(datetime.timezone.utc) + < datetime.datetime.now(datetime.UTC) .replace(minute=0, second=0, microsecond=0) .timestamp() ): @@ -577,7 +743,18 @@ class RecordingExporter(threading.Thread): ) ).split(" ") else: - chapters_path = self._build_chapter_metadata_file(recordings) + # Realtime/stream-copy export. Embed chapter metadata according to + # the camera's configured chapter mode: per-recording-segment + # timestamps or per-review-item titles. + if self.chapters == ChaptersEnum.recording_segments: + chapters_path = self._build_recording_segment_chapter_metadata_file( + recordings + ) + elif self.chapters == ChaptersEnum.review_items: + chapters_path = self._build_chapter_metadata_file(recordings) + else: + chapters_path = None + chapter_args = ( f" -i {chapters_path} -map 0 -dn -map_metadata 1" if chapters_path @@ -589,7 +766,19 @@ class RecordingExporter(threading.Thread): # add metadata title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" - ffmpeg_cmd.extend(["-metadata", f"title={title}"]) + creation_time = datetime.datetime.fromtimestamp( + self.start_time, tz=datetime.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) @@ -675,18 +864,32 @@ class RecordingExporter(threading.Thread): self.config.ffmpeg.ffmpeg_path, hwaccel_args, f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(), - f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(), + f"{self.ffmpeg_output_args} -movflags +faststart".strip(), EncodeTypeEnum.timelapse, ) ).split(" ") else: ffmpeg_cmd = ( - f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}" + f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart" ).split(" ") # add metadata title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}" - ffmpeg_cmd.extend(["-metadata", f"title={title}"]) + creation_time = datetime.datetime.fromtimestamp( + self.start_time, tz=datetime.UTC + ).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + ffmpeg_cmd.extend( + [ + "-metadata", + f"title={title}", + "-metadata", + f"creation_time={creation_time}", + "-metadata", + f"comment=Camera: {self.camera}", + ] + ) + + ffmpeg_cmd.append(video_path) return ffmpeg_cmd, playlist_lines diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 62d4ad8cb8..7f9dbc19da 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -11,7 +11,7 @@ import time from collections import defaultdict from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Any, Optional, Tuple +from typing import Any import numpy as np import psutil @@ -42,6 +42,8 @@ from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) +STALE_RECORDINGS_INFO_TTL = MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2 + class SegmentInfo: def __init__( @@ -98,7 +100,7 @@ class RecordingMaintainer(threading.Thread): self.stop_event = stop_event self.object_recordings_info: dict[str, list] = defaultdict(list) self.audio_recordings_info: dict[str, list] = defaultdict(list) - self.end_time_cache: dict[str, Tuple[datetime.datetime, float]] = {} + self.end_time_cache: dict[str, tuple[datetime.datetime, float]] = {} self.unexpected_cache_files_logged: bool = False async def move_files(self) -> None: @@ -125,7 +127,7 @@ class RecordingMaintainer(threading.Thread): start_time = datetime.datetime.strptime( date, CACHE_SEGMENT_FORMAT - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) if ( camera not in newest_cache_segments or start_time > newest_cache_segments[camera]["start_time"] @@ -185,7 +187,7 @@ class RecordingMaintainer(threading.Thread): # important that start_time is utc because recordings are stored and compared in utc start_time = datetime.datetime.strptime( date, CACHE_SEGMENT_FORMAT - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) grouped_recordings[camera].append( { @@ -301,9 +303,9 @@ class RecordingMaintainer(threading.Thread): RecordingsDataTypeEnum.saved.value, ) - recordings_to_insert: list[Optional[dict[str, Any]]] = await asyncio.gather( - *tasks - ) + self._expire_stale_recordings_info(grouped_recordings) + + recordings_to_insert: list[dict[str, Any] | None] = await asyncio.gather(*tasks) # fire and forget recordings entries self.requestor.send_data( @@ -311,13 +313,28 @@ 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) async def validate_and_move_segment( self, camera: str, reviews: Any, recording: dict[str, Any] - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: cache_path: str = recording["cache_path"] start_time: datetime.datetime = recording["start_time"] @@ -394,7 +411,7 @@ class RecordingMaintainer(threading.Thread): if ( datetime.datetime.fromtimestamp( most_recently_processed_frame_time - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) >= end_time ): record_mode = ( @@ -476,7 +493,7 @@ class RecordingMaintainer(threading.Thread): ) retain_cutoff = datetime.datetime.fromtimestamp( most_recently_processed_frame_time - record_config.event_pre_capture - ).astimezone(datetime.timezone.utc) + ).astimezone(datetime.UTC) if end_time < retain_cutoff: self.drop_segment(cache_path) @@ -602,7 +619,7 @@ class RecordingMaintainer(threading.Thread): duration: float, cache_path: str, segment_info: SegmentInfo, - ) -> Optional[dict[str, Any]]: + ) -> dict[str, Any] | None: # directory will be in utc due to start_time being in utc directory = os.path.join( RECORD_DIR, @@ -631,6 +648,8 @@ class RecordingMaintainer(threading.Thread): "copy", "-movflags", "+faststart", + "-metadata", + f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}", file_path, stderr=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.DEVNULL, diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index cfc59744c3..f3abc1bba3 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -11,7 +11,7 @@ import sys import threading from multiprocessing.synchronize import Event as MpEvent from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 import numpy as np @@ -267,7 +267,7 @@ class ReviewSegmentMaintainer(threading.Thread): def __init__(self, config: FrigateConfig, stop_event: MpEvent): super().__init__(name="review_segment_maintainer") self.config = config - self.active_review_segments: dict[str, Optional[PendingReviewSegment]] = {} + self.active_review_segments: dict[str, PendingReviewSegment | None] = {} self.frame_manager = SharedMemoryFrameManager() # create communication for review segments @@ -323,7 +323,7 @@ class ReviewSegmentMaintainer(threading.Thread): self, segment: PendingReviewSegment, camera_config: CameraConfig, - frame: Optional[np.ndarray], + frame: np.ndarray | None, objects: list[dict[str, Any]], prev_data: dict[str, Any], ) -> None: diff --git a/frigate/service_manager/multiprocessing.py b/frigate/service_manager/multiprocessing.py index 87bb4ffeea..88d497dd66 100644 --- a/frigate/service_manager/multiprocessing.py +++ b/frigate/service_manager/multiprocessing.py @@ -9,7 +9,6 @@ from abc import ABC, abstractmethod from asyncio.exceptions import TimeoutError from logging.handlers import QueueHandler from types import FrameType -from typing import Optional import frigate.log @@ -22,13 +21,13 @@ DEFAULT_STOP_TIMEOUT = 10 # seconds class BaseServiceProcess(Service, ABC): """A Service the manages a multiprocessing.Process.""" - _process: Optional[mp.Process] + _process: mp.Process | None def __init__( self, *, - name: Optional[str] = None, - manager: Optional[ServiceManager] = None, + name: str | None = None, + manager: ServiceManager | None = None, ) -> None: super().__init__(name=name, manager=manager) @@ -55,7 +54,7 @@ class BaseServiceProcess(Service, ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> None: if timeout is None: timeout = DEFAULT_STOP_TIMEOUT @@ -85,7 +84,7 @@ class BaseServiceProcess(Service, ABC): self.manager.logger.info(f"{self.name} stopped") @property - def pid(self) -> Optional[int]: + def pid(self) -> int | None: return self._process.pid if self._process else None def _run(self) -> None: @@ -143,7 +142,7 @@ class ServiceProcess(BaseServiceProcess): faulthandler.enable() - def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: + def receiveSignal(signalNumber: int, frame: FrameType | None) -> None: # Get the stop_event through the dict to bypass lazy initialization. stop_event = self.__dict__.get("stop_event") if stop_event is not None: diff --git a/frigate/service_manager/multiprocessing_waiter.py b/frigate/service_manager/multiprocessing_waiter.py index 8acdf583c7..e356fac1d6 100644 --- a/frigate/service_manager/multiprocessing_waiter.py +++ b/frigate/service_manager/multiprocessing_waiter.py @@ -7,7 +7,7 @@ import threading from multiprocessing.connection import Connection from multiprocessing.connection import wait as mp_wait from socket import socket -from typing import Any, Optional, Union +from typing import Any logger = logging.getLogger(__name__) @@ -118,10 +118,10 @@ class MultiprocessingWaiter(threading.Thread): waiter_lock = threading.Lock() -waiter_thread: Optional[MultiprocessingWaiter] = None +waiter_thread: MultiprocessingWaiter | None = None -async def wait(object: Union[mp.Process, Connection, socket]) -> None: +async def wait(object: mp.Process | Connection | socket) -> None: """Wait for the supplied object to be ready. Under the hood, this uses multiprocessing.connection.wait() and a background thread manage the @@ -129,7 +129,7 @@ async def wait(object: Union[mp.Process, Connection, socket]) -> None: """ global waiter_thread, waiter_lock - sentinel: Union[Connection, socket, int] + sentinel: Connection | socket | int if isinstance(object, mp.Process): sentinel = object.sentinel elif isinstance(object, Connection) or isinstance(object, socket): diff --git a/frigate/service_manager/service.py b/frigate/service_manager/service.py index 89d766e9d6..34631b6210 100644 --- a/frigate/service_manager/service.py +++ b/frigate/service_manager/service.py @@ -5,12 +5,11 @@ import atexit import logging import threading from abc import ABC, abstractmethod +from collections.abc import Coroutine from contextvars import ContextVar from dataclasses import dataclass from functools import partial -from typing import Coroutine, Optional, Union, cast - -from typing_extensions import Self +from typing import Self, cast class Service(ABC): @@ -19,8 +18,8 @@ class Service(ABC): def __init__( self, *, - name: Optional[str] = None, - manager: Optional[ServiceManager] = None, + name: str | None = None, + manager: ServiceManager | None = None, ): if name: self.__dict__["name"] = name @@ -42,13 +41,13 @@ class Service(ABC): try: return self.__manager except AttributeError: - raise RuntimeError("Cannot access associated service manager") + raise RuntimeError("Cannot access associated service manager") from None def start( self, *, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Start this service. @@ -70,9 +69,9 @@ class Service(ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Stop this service. @@ -97,9 +96,9 @@ class Service(ABC): self, *, force: bool = False, - stop_timeout: Optional[float] = None, + stop_timeout: float | None = None, wait: bool = False, - wait_timeout: Optional[float] = None, + wait_timeout: float | None = None, ) -> Self: """Restart this service. @@ -129,7 +128,7 @@ class Service(ABC): self, *, force: bool = False, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> None: pass @@ -137,14 +136,14 @@ class Service(ABC): self, *, force: bool = False, - stop_timeout: Optional[float] = None, + stop_timeout: float | None = None, ) -> None: await self.on_stop(force=force, timeout=stop_timeout) await self.on_start() default_service_manager_lock = threading.Lock() -default_service_manager: Optional[ServiceManager] = None +default_service_manager: ServiceManager | None = None current_service_manager: ContextVar[ServiceManager] = ContextVar( "current_service_manager" @@ -162,8 +161,8 @@ class Command: """ coro: Coroutine - lock: Optional[asyncio.Lock] = None - done: Optional[threading.Event] = None + lock: asyncio.Lock | None = None + done: threading.Event | None = None class ServiceManager: @@ -189,7 +188,7 @@ class ServiceManager: _services_lock: threading.Lock # Commands will be queued with associated event loop. Queueing `None` signals shutdown. - _command_queue: asyncio.Queue[Union[Command, None]] + _command_queue: asyncio.Queue[Command | None] _event_loop: asyncio.AbstractEventLoop # The pending command counter is used to ensure all commands have been queued before shutdown. @@ -204,7 +203,7 @@ class ServiceManager: # Will be acquired to ensure the shutdown sentinel is sent only once. Never released. _shutdown_lock: threading.Lock - def __init__(self, *, name: Optional[str] = None): + def __init__(self, *, name: str | None = None): self._name = name if name is not None else (__package__ or __name__) self._logger = logging.getLogger(self.name) @@ -276,8 +275,8 @@ class ServiceManager: coro: Coroutine, *, wait: bool = False, - wait_timeout: Optional[float] = None, - lock: Optional[asyncio.Lock] = None, + wait_timeout: float | None = None, + lock: asyncio.Lock | None = None, ) -> None: """Run an async task in the service manager thread. @@ -299,7 +298,7 @@ class ServiceManager: cmd.done.wait(timeout=wait_timeout) def shutdown( - self, *, wait: bool = False, wait_timeout: Optional[float] = None + self, *, wait: bool = False, wait_timeout: float | None = None ) -> None: """Shutdown the service manager thread. @@ -321,7 +320,7 @@ class ServiceManager: if not self._manager_thread.is_alive(): raise RuntimeError(f"ServiceManager {self.name} is not running") - def _send_command(self, command: Union[Command, None]) -> None: + def _send_command(self, command: Command | None) -> None: self._ensure_running() async def queue_command() -> None: @@ -336,7 +335,7 @@ class ServiceManager: self._ensure_running() with self._services_lock: - name_conflict: Optional[Service] = next( + name_conflict: Service | None = next( ( existing for name, existing in self._services.items() diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 13f50c5868..7c2e328c73 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -6,7 +6,7 @@ import logging import threading import time from multiprocessing.synchronize import Event as MpEvent -from typing import Any, Optional +from typing import Any from frigate.comms.inter_process import InterProcessRequestor from frigate.config import FrigateConfig @@ -49,9 +49,7 @@ class StatsEmitter(threading.Thread): self.stats_history.append(stats) return stats - def get_stats_history( - self, keys: Optional[list[str]] = None - ) -> list[dict[str, Any]]: + def get_stats_history(self, keys: list[str] | None = None) -> list[dict[str, Any]]: """Get stats history. Supports dot-notation for nested keys to avoid returning large objects diff --git a/frigate/stats/intel_gpu_info.py b/frigate/stats/intel_gpu_info.py index 5ca3066fbd..5bbb5c335b 100644 --- a/frigate/stats/intel_gpu_info.py +++ b/frigate/stats/intel_gpu_info.py @@ -2,7 +2,6 @@ import logging import re -from typing import Optional logger = logging.getLogger(__name__) @@ -16,7 +15,7 @@ class IntelGpuNameResolver: name with the pdev string used by DRM fdinfo. """ - _names: Optional[dict[str, str]] = None + _names: dict[str, str] | None = None def get_names(self) -> dict[str, str]: if self._names is not None: @@ -39,7 +38,7 @@ class IntelGpuNameResolver: self._names = names return names - cpu_name: Optional[str] = None + cpu_name: str | None = None if "CPU" in devices: try: cpu_name = self._strip_trademarks( @@ -70,14 +69,14 @@ class IntelGpuNameResolver: return names @staticmethod - def _format_pdev(pci) -> Optional[str]: + def _format_pdev(pci) -> str | None: try: return f"{pci.domain:04x}:{pci.bus:02x}:{pci.device:02x}.{pci.function:x}" except AttributeError: return None @classmethod - def _resolve_name(cls, raw_name: str, device_type, cpu_name: Optional[str]) -> str: + def _resolve_name(cls, raw_name: str, device_type, cpu_name: str | None) -> str: """Build a display name for a GPU. Modern integrated Intel GPUs are reported by OpenVINO with a generic diff --git a/frigate/stats/prometheus.py b/frigate/stats/prometheus.py index d2e2295686..9797d569e1 100644 --- a/frigate/stats/prometheus.py +++ b/frigate/stats/prometheus.py @@ -1,6 +1,6 @@ import logging import re -from typing import Any, Dict, List +from typing import Any from prometheus_client import CONTENT_TYPE_LATEST, generate_latest from prometheus_client.core import ( @@ -11,7 +11,7 @@ from prometheus_client.core import ( ) -class CustomCollector(object): +class CustomCollector: def __init__(self, _url): self.complete_stats = {} # Store complete stats data self.process_stats = {} # Keep for CPU processing @@ -491,7 +491,7 @@ collector = CustomCollector(None) REGISTRY.register(collector) -def update_metrics(stats: Dict[str, Any], event_counts: List[Dict[str, Any]]): +def update_metrics(stats: dict[str, Any], event_counts: list[dict[str, Any]]): """Updates the Prometheus metrics with the given stats data.""" try: # Store the complete stats for later use by collect() diff --git a/frigate/stats/util.py b/frigate/stats/util.py index 56efce5d01..d3cb81986a 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -7,7 +7,7 @@ import shutil import time from json import JSONDecodeError from multiprocessing.managers import DictProxy -from typing import Any, Optional +from typing import Any import requests from requests.exceptions import RequestException @@ -78,7 +78,7 @@ def stats_init( return stats_tracking -def read_temperature(path: str) -> Optional[float]: +def read_temperature(path: str) -> float | None: if os.path.isfile(path): with open(path) as f: line = f.readline().strip() @@ -106,7 +106,7 @@ def get_temperatures() -> dict[str, float]: def get_detector_temperature( detector_type: str, detector_index_by_type: dict[str, int], -) -> Optional[float]: +) -> float | None: """Get temperature for a specific detector based on its type.""" if detector_type == "edgetpu": # Get temperatures for all attached Corals diff --git a/frigate/test/http_api/test_http_keyframe_analysis.py b/frigate/test/http_api/test_http_keyframe_analysis.py new file mode 100644 index 0000000000..6a49e105ec --- /dev/null +++ b/frigate/test/http_api/test_http_keyframe_analysis.py @@ -0,0 +1,58 @@ +from unittest.mock import AsyncMock, patch + +from frigate.models import Event, Recordings, ReviewSegment +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +class TestHttpKeyframeAnalysis(BaseTestHttp): + def setUp(self): + super().setUp([Event, Recordings, ReviewSegment]) + + def test_invalid_camera_returns_404(self): + app = super().create_app() + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=does_not_exist") + assert response.status_code == 404 + + def test_record_disabled_returns_neutral(self): + # default minimal_config has recording disabled + app = super().create_app() + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=front_door") + assert response.status_code == 200 + assert response.json()["severity"] == "record_disabled" + + def test_probes_record_input_and_returns_severity(self): + self.minimal_config["cameras"]["front_door"]["ffmpeg"]["inputs"] = [ + { + "path": "rtsp://10.0.0.1:554/record", + "roles": ["detect", "record"], + } + ] + self.minimal_config["cameras"]["front_door"]["record"] = {"enabled": True} + app = super().create_app() + + canned = { + "severity": "ok", + "keyframe_count": 5, + "max_gap": 1.0, + "mean_gap": 1.0, + "min_gap": 1.0, + "segment_time": 10, + "duration_observed": 4.0, + "thresholds": {"warning": 4.0, "error": 10}, + } + + with patch( + "frigate.api.camera.analyze_record_keyframes", + AsyncMock(return_value=canned), + ) as mock_probe: + with AuthTestClient(app) as client: + response = client.get("/keyframe_analysis?camera=front_door") + + assert response.status_code == 200 + assert response.json()["severity"] == "ok" + # index matches the input carrying the record role ("Stream 1") + assert response.json()["stream_index"] == 0 + # the record-role input path was probed + assert mock_probe.await_args.args[1] == "rtsp://10.0.0.1:554/record" diff --git a/frigate/test/http_api/test_http_media.py b/frigate/test/http_api/test_http_media.py index 58f9f92477..b2d83cbc8a 100644 --- a/frigate/test/http_api/test_http_media.py +++ b/frigate/test/http_api/test_http_media.py @@ -1,6 +1,6 @@ """Unit tests for recordings/media API endpoints.""" -from datetime import datetime, timezone +from datetime import UTC, datetime import pytz from fastapi import Request @@ -306,8 +306,8 @@ class TestHttpMedia(BaseTestHttp): Test recordings summary with UTC timezone (no DST). """ # Use UTC timestamps directly - march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=timezone.utc).timestamp() - march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=timezone.utc).timestamp() + march_9_utc = datetime(2024, 3, 9, 17, 0, 0, tzinfo=UTC).timestamp() + march_10_utc = datetime(2024, 3, 10, 17, 0, 0, tzinfo=UTC).timestamp() with AuthTestClient(self.app) as client: Recordings.insert( @@ -475,3 +475,55 @@ class TestHttpMedia(BaseTestHttp): assert response.status_code == 200 assert response.json() == [] + + def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self): + """cameras=all must not error and must only consider allowed cameras. + + allowed_cameras is mocked to ["front_door"]. A back_door recording that + would otherwise fill the gap must be ignored, and the request must not + 500 the way it did when cameras was reassigned to a list. + """ + with AuthTestClient(self.app) as client: + # front_door has a 20s gap (1010-1030). + Recordings.insert( + id="front_a", + path="/media/recordings/front_a.mp4", + camera="front_door", + start_time=1000, + end_time=1010, + duration=10, + motion=0, + ).execute() + Recordings.insert( + id="front_b", + path="/media/recordings/front_b.mp4", + camera="front_door", + start_time=1030, + end_time=1040, + duration=10, + motion=0, + ).execute() + # back_door is not in allowed_cameras; its full-window coverage must + # not mask the front_door gap. + Recordings.insert( + id="back_a", + path="/media/recordings/back_a.mp4", + camera="back_door", + start_time=1000, + end_time=1040, + duration=40, + motion=0, + ).execute() + + response = client.get( + "/recordings/unavailable", + params={ + "after": 1000, + "before": 1040, + "scale": 5, + "cameras": "all", + }, + ) + + assert response.status_code == 200 + assert response.json() == [{"start_time": 1010, "end_time": 1030}] diff --git a/frigate/test/test_birdseye.py b/frigate/test/test_birdseye.py index 33683f5c4b..bd70e37efe 100644 --- a/frigate/test/test_birdseye.py +++ b/frigate/test/test_birdseye.py @@ -1,8 +1,10 @@ """Test camera user and password cleanup.""" +import multiprocessing as mp import unittest -from frigate.output.birdseye import get_canvas_shape +from frigate.config import FrigateConfig +from frigate.output.birdseye import BirdsEyeFrameManager, get_canvas_shape class TestBirdseye(unittest.TestCase): @@ -45,3 +47,70 @@ class TestBirdseye(unittest.TestCase): canvas_width, canvas_height = get_canvas_shape(width, height) assert canvas_width == width # width will be the same assert canvas_height != height + + +class TestBirdseyeCameraOrder(unittest.TestCase): + """Test that birdseye reacts to camera order changes without a restart.""" + + def setUp(self): + config = { + "mqtt": {"enabled": False}, + "birdseye": {"enabled": True, "mode": "continuous"}, + "cameras": { + camera: { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + } + for camera in ("back", "front", "side") + }, + } + self.config = FrigateConfig(**config) + self.manager = BirdsEyeFrameManager(self.config, mp.Event()) + + # mark every camera as continuously active with no frame to draw, which + # exercises the layout without needing real yuv frames + for camera_data in self.manager.cameras.values(): + camera_data["current_frame"] = None + camera_data["current_frame_time"] = 1.0 + camera_data["last_active_frame"] = 1.0 + + def layout_order(self) -> list[str]: + """Return the cameras in the order the current layout renders them.""" + return [position[0] for row in self.manager.camera_layout for position in row] + + def test_layout_uses_configured_order(self): + """Test the layout is sorted by order, then by name when tied.""" + self.config.cameras["side"].birdseye.order = 0 + self.config.cameras["back"].birdseye.order = 10 + self.config.cameras["front"].birdseye.order = 20 + + self.manager.update_frame() + + assert self.layout_order() == ["side", "back", "front"] + + def test_order_change_rebuilds_layout(self): + """Test a reorder relayouts even though the active cameras are unchanged.""" + self.manager.update_frame() + assert self.layout_order() == ["back", "front", "side"] + + # a stable active set means only an order change can reset the layout, + # which is what a settings reorder publishes to this process + self.config.cameras["side"].birdseye.order = -10 + + _, layout_changed = self.manager.update_frame() + + assert layout_changed + assert self.layout_order() == ["side", "back", "front"] + + def test_unchanged_order_keeps_layout(self): + """Test a repeat update with no order change doesn't reset the layout.""" + self.manager.update_frame() + + _, layout_changed = self.manager.update_frame() + + assert not layout_changed + assert self.layout_order() == ["back", "front", "side"] diff --git a/frigate/test/test_builtin.py b/frigate/test/test_builtin.py new file mode 100644 index 0000000000..7cf47de5c6 --- /dev/null +++ b/frigate/test/test_builtin.py @@ -0,0 +1,41 @@ +"""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() diff --git a/frigate/test/test_classification_enabled.py b/frigate/test/test_classification_enabled.py new file mode 100644 index 0000000000..abb139cd08 --- /dev/null +++ b/frigate/test/test_classification_enabled.py @@ -0,0 +1,106 @@ +"""Tests that disabled custom classification models are not registered or run.""" + +import sys +import unittest +from unittest.mock import MagicMock + +# Mock TFLite before importing the maintainer / classification modules +_MOCK_MODULES = [ + "tflite_runtime", + "tflite_runtime.interpreter", + "ai_edge_litert", + "ai_edge_litert.interpreter", +] +for mod in _MOCK_MODULES: + if mod not in sys.modules: + sys.modules[mod] = MagicMock() + +from frigate.data_processing.real_time.custom_classification import ( # noqa: E402 + CustomObjectClassificationProcessor, +) +from frigate.embeddings.maintainer import EmbeddingMaintainer # noqa: E402 + + +class TestCustomClassificationEnabledGating(unittest.TestCase): + """A model with enabled: false must not keep a processor registered.""" + + def _make_maintainer(self) -> EmbeddingMaintainer: + # Bypass the heavy __init__; only the attributes touched by the + # config update path are needed for these tests. + maintainer = EmbeddingMaintainer.__new__(EmbeddingMaintainer) + maintainer.realtime_processors = [] + maintainer.config = MagicMock() + maintainer.config.classification.custom = {} + maintainer.requestor = MagicMock() + maintainer.metrics = MagicMock() + maintainer.event_metadata_publisher = MagicMock() + return maintainer + + def _make_model_config(self, name: str, enabled: bool) -> MagicMock: + model_config = MagicMock() + model_config.name = name + model_config.enabled = enabled + model_config.state_config = None + return model_config + + def _make_processor(self, name: str) -> MagicMock: + processor = MagicMock(spec=CustomObjectClassificationProcessor) + processor.model_config = MagicMock() + processor.model_config.name = name + return processor + + def test_disabled_update_tears_down_existing_processor(self): + """Toggling a running model to disabled shuts down and drops its processor.""" + maintainer = self._make_maintainer() + processor = self._make_processor("atli") + maintainer.realtime_processors = [processor] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + processor.shutdown.assert_called_once() + self.assertEqual(maintainer.realtime_processors, []) + + def test_disabled_update_does_not_register_processor(self): + """A disabled model that has no processor is never registered.""" + maintainer = self._make_maintainer() + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + self.assertEqual(maintainer.realtime_processors, []) + + def test_disabled_update_leaves_other_processors_untouched(self): + """Disabling one model must not affect other running processors.""" + maintainer = self._make_maintainer() + other = self._make_processor("simbi") + maintainer.realtime_processors = [other] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", + self._make_model_config("atli", enabled=False), + ) + + other.shutdown.assert_not_called() + self.assertEqual(maintainer.realtime_processors, [other]) + + def test_removed_model_tears_down_processor(self): + """A None payload (model deleted) still shuts down its processor.""" + maintainer = self._make_maintainer() + processor = self._make_processor("atli") + maintainer.realtime_processors = [processor] + + maintainer._handle_custom_classification_update( + "config/classification/custom/atli", None + ) + + processor.shutdown.assert_called_once() + self.assertEqual(maintainer.realtime_processors, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_config.py b/frigate/test/test_config.py index 6490a65099..95884342be 100644 --- a/frigate/test/test_config.py +++ b/frigate/test/test_config.py @@ -397,6 +397,43 @@ class TestConfig(unittest.TestCase): assert "dog" in frigate_config.cameras["back"].objects.filters assert frigate_config.cameras["back"].objects.filters["dog"].threshold == 0.7 + def test_unsupported_tracked_object_pruned_from_track_and_filters(self): + # "unicorn" is not in the model labelmap, so it must be removed from the + # tracked objects AND from the object filters, otherwise a stale filter + # entry lingers in the parsed config. + config = { + "mqtt": {"host": "mqtt"}, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": { + "height": 1080, + "width": 1920, + "fps": 5, + }, + "objects": { + "track": ["person", "unicorn"], + "filters": { + "person": {"threshold": 0.7}, + "unicorn": {"threshold": 0.7}, + }, + }, + } + }, + } + + frigate_config = FrigateConfig(**config) + objects = frigate_config.cameras["back"].objects + assert "unicorn" not in objects.track + assert "unicorn" not in objects.filters + # supported entries are left untouched + assert "person" in objects.track + assert "person" in objects.filters + def test_global_object_mask(self): config = { "mqtt": {"host": "mqtt"}, diff --git a/frigate/test/test_export.py b/frigate/test/test_export.py new file mode 100644 index 0000000000..7612a4144f --- /dev/null +++ b/frigate/test/test_export.py @@ -0,0 +1,132 @@ +import unittest + +from frigate.record.export import validate_ffmpeg_args + + +class TestValidateFfmpegArgs(unittest.TestCase): + """Tests for the non-admin custom export ffmpeg arg validator. + + The validator uses a structural allowlist: every token must be an + allowlisted flag or the value of one, filter values are restricted to a + safe set of filters, and no token may become a bare input/output URL. + """ + + def assertRejected(self, args: str) -> None: + valid, message = validate_ffmpeg_args(args) + self.assertFalse(valid, f"expected {args!r} to be rejected") + self.assertNotEqual(message, "") + + def assertAllowed(self, args: str) -> None: + valid, message = validate_ffmpeg_args(args) + self.assertTrue(valid, f"expected {args!r} to be allowed, got: {message}") + self.assertEqual(message, "") + + # --- legitimate use cases must keep working --------------------------- + + def test_timelapse_setpts_allowed(self): + # The whole reason -vf cannot simply be blocked: timelapse exports. + self.assertAllowed("-vf setpts=PTS/60 -r 25") + self.assertAllowed("-vf setpts=0.04*PTS -r 30") # server default + self.assertAllowed("-filter:v setpts=PTS/60 -r 25") + + def test_default_input_args_allowed(self): + self.assertAllowed("") + self.assertAllowed("-an -skip_frame nokey") + + def test_encoding_args_allowed(self): + self.assertAllowed("-c:v libx264 -crf 23 -preset fast") + self.assertAllowed("-c:v copy -c:a copy") + self.assertAllowed("-c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M") + self.assertAllowed("-movflags +faststart") + self.assertAllowed("-pix_fmt yuv420p -r 30 -g 30") + + def test_safe_filters_allowed(self): + self.assertAllowed("-vf scale=640:480") + self.assertAllowed("-vf scale=640:480,setpts=0.5*PTS") + self.assertAllowed("-vf format=yuv420p") + self.assertAllowed("-vf transpose=1") + self.assertAllowed("-vf hflip") + self.assertAllowed("-vf fps=15") + self.assertAllowed("-vf setsar=1 -an") + self.assertAllowed("-vf setdar=16/9") + + # --- the reported advisory and file-read class ------------------------ + + def test_reported_advisory_rejected(self): + self.assertRejected( + "-filter:v drawtext=textfile=/etc/passwd:fontcolor=white:fontsize=20" + ) + + def test_file_reading_filters_rejected(self): + self.assertRejected("-vf movie=/etc/passwd") + self.assertRejected("-vf drawtext=textfile=/etc/passwd") + self.assertRejected("-vf subtitles=/etc/passwd") + # marker embedded as an option of an otherwise-allowed filter name + self.assertRejected("-vf scale=movie=/etc/passwd") + + def test_filtergraph_brackets_rejected(self): + # link labels aren't needed for safe filters; rejecting "[" / "]" keeps + # filtergraph validation linear (no ReDoS on attacker input) + self.assertRejected("-vf [in]scale=640:480[out]") + self.assertRejected("-vf " + "[" * 5000) + + def test_preset_file_read_rejected(self): + # cwd-anchored traversal slipped past the old startswith() path check + self.assertRejected("-fpre frigate/../../../etc/passwd") + self.assertRejected("-fpre evil.preset") + self.assertRejected("-vpre x") + self.assertRejected("-apre x") + self.assertRejected("-pre x") + + def test_slash_option_file_read_rejected(self): + # ffmpeg "-/option file" reads the option value from a file + self.assertRejected("-/filter:v graph.txt") + self.assertRejected("-/filter_complex graph.txt") + + # --- network / SSRF class --------------------------------------------- + + def test_schemeless_protocol_rejected(self): + self.assertRejected("-f mpegts tcp:10.0.0.5:4444") + self.assertRejected("tcp:10.0.0.5:4444") + self.assertRejected("udp:10.0.0.5:4444") + self.assertRejected("-progress http:attacker.example.com:80/p") + + # --- file-write class -------------------------------------------------- + + def test_tee_write_rejected(self): + self.assertRejected("-c:v libx264 -map 0 -f tee [f=mpegts]/tmp/owned.ts") + self.assertRejected("-f tee [f=mpegts]/etc/frigate/x.ts") + self.assertRejected("tee:/tmp/x") + + def test_bare_output_token_rejected(self): + self.assertRejected("evil.mp4") + self.assertRejected("-c copy evil.mp4") + self.assertRejected("x/../escaped.mkv") + + def test_file_producing_muxers_rejected(self): + self.assertRejected("-f hls -hls_segment_filename pwn%03d.ts out.m3u8") + self.assertRejected("-f md5 victim.txt") + self.assertRejected("-f segment seg%03d.ts") + + def test_write_flags_rejected(self): + self.assertRejected("-progress evil.log") + self.assertRejected("-stats_enc_pre evil.csv") + self.assertRejected("-report") + + # --- resource exhaustion / misc --------------------------------------- + + def test_dos_input_flags_rejected(self): + self.assertRejected("-stream_loop -1") + self.assertRejected("-readrate 0.001") + + def test_disallowed_flags_rejected(self): + self.assertRejected("-map 0") + self.assertRejected("-i /etc/passwd") + self.assertRejected("-attach evil.bin") + self.assertRejected("-dump_attachment evil.bin") + self.assertRejected("/etc/passwd") + self.assertRejected("-metadata comment=x") + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_export_progress.py b/frigate/test/test_export_progress.py index 835cf91b99..81dcdb96e2 100644 --- a/frigate/test/test_export_progress.py +++ b/frigate/test/test_export_progress.py @@ -416,7 +416,7 @@ class TestSaveThumbnailFromPreviewFrames(unittest.TestCase): # comparison takes the current-hour branch (preview frames). import datetime - now = datetime.datetime.now(datetime.timezone.utc).timestamp() + now = datetime.datetime.now(datetime.UTC).timestamp() exporter = _make_exporter() exporter.export_id = "thumb_short" exporter.start_time = now diff --git a/frigate/test/test_genai_providers.py b/frigate/test/test_genai_providers.py new file mode 100644 index 0000000000..d73d632f0a --- /dev/null +++ b/frigate/test/test_genai_providers.py @@ -0,0 +1,496 @@ +"""Smoke tests for GenAI chat providers. + +Each provider's ``chat_with_tools_stream`` is driven with a canned "test +response" so the two conversion layers are exercised without any network: + + 1. Frigate (OpenAI-style) messages -> provider-native request format + 2. provider-native response -> Frigate ``("kind", value)`` stream events + +These guard against regressions such as tool-call arguments arriving as raw +strings instead of dicts (which crash the ``ToolCall`` model), and multimodal +user content (a list of text/image parts, as injected by ``get_live_context``) +crashing message conversion. +""" + +import asyncio +import base64 +import json +import unittest +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock, patch + +from frigate.config import GenAIConfig, GenAIProviderEnum +from frigate.genai import PROVIDERS, load_providers + +load_providers() + +# A minimal but valid JPEG data URI, mirroring what get_live_context injects. +_TINY_JPEG = base64.b64encode(b"\xff\xd8\xff\xd9").decode("ascii") +_IMAGE_DATA_URI = f"data:image/jpeg;base64,{_TINY_JPEG}" + +# Conversation ending in a multimodal user message (text + live image), the +# exact shape the chat endpoint builds after a get_live_context tool result. +MULTIMODAL_MESSAGES = [ + {"role": "system", "content": "You are a test assistant."}, + {"role": "user", "content": "what do you see on the front camera?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "get_live_context", + "arguments": json.dumps({"camera": "front"}), + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_1", + "name": "get_live_context", + "content": json.dumps({"camera": "front"}), + }, + { + "role": "user", + "content": [ + { + "type": "text", + "text": "Here is the current live image from camera 'front'.", + }, + {"type": "image_url", "image_url": {"url": _IMAGE_DATA_URI}}, + ], + }, +] + +SIMPLE_MESSAGES = [ + {"role": "system", "content": "You are a test assistant."}, + {"role": "user", "content": "hello"}, +] + +TOOLS = [ + { + "type": "function", + "function": { + "name": "search_objects", + "description": "Search tracked objects", + "parameters": { + "type": "object", + "properties": {"label": {"type": "string"}}, + }, + }, + } +] + + +def _make_client(provider: str, **cfg_overrides): + """Build a provider client offline (no model validation, no network).""" + cfg = GenAIConfig(provider=provider, **cfg_overrides) + cls = PROVIDERS[GenAIProviderEnum(provider)] + return cls(cfg, timeout=5, validate_model=False) + + +def _collect(client, messages, tools=TOOLS): + """Drain chat_with_tools_stream into a list of (kind, value) events.""" + + async def _run(): + events = [] + async for event in client.chat_with_tools_stream( + messages=messages, tools=tools, tool_choice="auto" + ): + events.append(event) + return events + + return asyncio.run(_run()) + + +def _final_message(events) -> dict: + messages = [value for (kind, value) in events if kind == "message"] + assert messages, f"stream produced no final message: {events}" + return messages[-1] + + +def _assert_tool_args_are_dicts(final: dict) -> None: + """Every returned tool call must expose arguments as a dict, never a string.""" + for tool_call in final.get("tool_calls") or []: + assert isinstance(tool_call["arguments"], dict), ( + f"tool call arguments must be a dict, got " + f"{type(tool_call['arguments']).__name__}: {tool_call['arguments']!r}" + ) + + +# --------------------------------------------------------------------------- +# OpenAI +# --------------------------------------------------------------------------- +def _openai_tc(index, id=None, name=None, arguments=None): + return SimpleNamespace( + index=index, + id=id, + function=SimpleNamespace(name=name, arguments=arguments), + ) + + +def _openai_chunk(content=None, tool_calls=None, finish_reason=None, usage=None): + delta = SimpleNamespace( + content=content, + tool_calls=tool_calls, + reasoning_content=None, + reasoning=None, + ) + choice = SimpleNamespace(delta=delta, finish_reason=finish_reason) + return SimpleNamespace(choices=[choice], usage=usage) + + +class TestOpenAIProvider(unittest.TestCase): + def _client(self): + return _make_client( + "openai", model="gpt-4o", api_key="k", base_url="http://localhost:9999/v1" + ) + + def test_stream_tool_call_arguments_are_dict(self): + # Arguments arrive split across chunks, as the real API streams them. + chunks = [ + _openai_chunk( + tool_calls=[ + _openai_tc(0, id="c1", name="search_objects", arguments='{"label":') + ] + ), + _openai_chunk(tool_calls=[_openai_tc(0, arguments=' "person"}')]), + _openai_chunk(finish_reason="tool_calls"), + ] + client = self._client() + client.provider.chat.completions.create = MagicMock(return_value=iter(chunks)) + + final = _final_message(_collect(client, SIMPLE_MESSAGES)) + self.assertEqual(final["finish_reason"], "tool_calls") + self.assertEqual(len(final["tool_calls"]), 1) + _assert_tool_args_are_dicts(final) + self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"}) + + def test_stream_content_response(self): + chunks = [ + _openai_chunk(content="hel"), + _openai_chunk(content="lo"), + _openai_chunk(finish_reason="stop"), + ] + client = self._client() + client.provider.chat.completions.create = MagicMock(return_value=iter(chunks)) + + events = _collect(client, SIMPLE_MESSAGES) + deltas = [v for (k, v) in events if k == "content_delta"] + self.assertEqual("".join(deltas), "hello") + self.assertEqual(_final_message(events)["content"], "hello") + + def test_multimodal_message_does_not_crash(self): + client = self._client() + client.provider.chat.completions.create = MagicMock( + return_value=iter([_openai_chunk(content="ok", finish_reason="stop")]) + ) + # Passing the OpenAI-native multimodal list through must not raise. + final = _final_message(_collect(client, MULTIMODAL_MESSAGES)) + self.assertEqual(final["content"], "ok") + + +# --------------------------------------------------------------------------- +# Gemini +# --------------------------------------------------------------------------- +def _gemini_part(text=None, thought=False, function_call=None, thought_signature=None): + return SimpleNamespace( + text=text, + thought=thought, + function_call=function_call, + thought_signature=thought_signature, + ) + + +def _gemini_chunk(parts, finish_reason=None, usage_metadata=None): + candidate = SimpleNamespace( + content=SimpleNamespace(parts=parts), finish_reason=finish_reason + ) + return SimpleNamespace(candidates=[candidate], usage_metadata=usage_metadata) + + +def _gemini_stream(chunks): + async def _agen(*args, **kwargs): + for chunk in chunks: + yield chunk + + return _agen + + +class TestGeminiProvider(unittest.TestCase): + def _client(self): + return _make_client("gemini", model="gemini-2.5-flash", api_key="k") + + def _patch_stream(self, client, chunks): + client.provider = MagicMock() + client.provider.aio.models.generate_content_stream = AsyncMock( + side_effect=_gemini_stream(chunks) + ) + + def test_stream_parallel_tool_calls_stay_separate_dicts(self): + # Regression: Gemini streams complete function calls. Two calls to the + # same tool must NOT be merged into one concatenated arguments string. + from google.genai.types import FinishReason + + chunks = [ + _gemini_chunk( + parts=[ + _gemini_part( + function_call=SimpleNamespace( + name="search_objects", args={"label": "person"} + ) + ), + _gemini_part( + function_call=SimpleNamespace( + name="search_objects", args={"limit": 1} + ) + ), + ], + finish_reason=FinishReason.STOP, + ), + ] + client = self._client() + self._patch_stream(client, chunks) + + final = _final_message(_collect(client, SIMPLE_MESSAGES)) + self.assertEqual(final["finish_reason"], "tool_calls") + self.assertEqual(len(final["tool_calls"]), 2) + _assert_tool_args_are_dicts(final) + self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"}) + self.assertEqual(final["tool_calls"][1]["arguments"], {"limit": 1}) + + def test_stream_content_response(self): + from google.genai.types import FinishReason + + chunks = [ + _gemini_chunk(parts=[_gemini_part(text="hel")]), + _gemini_chunk( + parts=[_gemini_part(text="lo")], finish_reason=FinishReason.STOP + ), + ] + client = self._client() + self._patch_stream(client, chunks) + + events = _collect(client, SIMPLE_MESSAGES) + deltas = [v for (k, v) in events if k == "content_delta"] + self.assertEqual("".join(deltas), "hello") + self.assertEqual(_final_message(events)["content"], "hello") + + def test_multimodal_message_converts_without_crash(self): + # Regression: a user message with list content (text + image_url) used + # to be handed to Part.from_text(text=) and raise ValidationError. + from google.genai.types import FinishReason + + client = self._client() + self._patch_stream( + client, + [ + _gemini_chunk( + parts=[_gemini_part(text="ok")], finish_reason=FinishReason.STOP + ) + ], + ) + final = _final_message(_collect(client, MULTIMODAL_MESSAGES)) + self.assertEqual(final["content"], "ok") + + +# --------------------------------------------------------------------------- +# Ollama +# --------------------------------------------------------------------------- +class TestOllamaProvider(unittest.TestCase): + def _client(self): + return _make_client("ollama", model="llama3", base_url="http://localhost:9999") + + def _run_with_response(self, client, response, messages): + # Ollama uses a non-streaming call when tools are present, via an + # internally-constructed async client. + fake_async = MagicMock() + fake_async.chat = AsyncMock(return_value=response) + with patch( + "frigate.genai.plugins.ollama.OllamaAsyncClient", + return_value=fake_async, + ): + return _collect(client, messages) + + def test_tool_call_arguments_are_dict(self): + response = { + "message": { + "content": "", + "tool_calls": [ + { + "function": { + "name": "search_objects", + "arguments": {"label": "person"}, + } + } + ], + }, + "done": True, + "done_reason": "stop", + "eval_count": 5, + "prompt_eval_count": 3, + "eval_duration": 1_000_000, + } + client = self._client() + final = _final_message( + self._run_with_response(client, response, SIMPLE_MESSAGES) + ) + self.assertEqual(final["finish_reason"], "tool_calls") + _assert_tool_args_are_dicts(final) + self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"}) + + def test_multimodal_message_normalizes_image(self): + # Ollama needs content as a string with images pulled into a separate + # field; the normalizer must extract both without crashing. + response = { + "message": {"content": "ok"}, + "done": True, + "done_reason": "stop", + } + client = self._client() + final = _final_message( + self._run_with_response(client, response, MULTIMODAL_MESSAGES) + ) + self.assertEqual(final["content"], "ok") + + def test_normalize_multimodal_content(self): + from frigate.genai.plugins.ollama import _normalize_multimodal_content + + text, images = _normalize_multimodal_content(MULTIMODAL_MESSAGES[-1]["content"]) + self.assertIn("live image", text) + self.assertEqual(len(images), 1) + self.assertEqual(images[0], b"\xff\xd8\xff\xd9") + + +# --------------------------------------------------------------------------- +# llama.cpp +# --------------------------------------------------------------------------- +class _FakeStreamResponse: + def __init__(self, lines): + self._lines = lines + + def raise_for_status(self): + return None + + async def aiter_lines(self): + for line in self._lines: + yield line + + +class _FakeStreamCtx: + def __init__(self, lines): + self._resp = _FakeStreamResponse(lines) + + async def __aenter__(self): + return self._resp + + async def __aexit__(self, *exc): + return False + + +class _FakeAsyncClient: + def __init__(self, lines): + self._lines = lines + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + return False + + def stream(self, method, url, json=None, headers=None): + return _FakeStreamCtx(self._lines) + + +class TestLlamaCppProvider(unittest.TestCase): + def _client(self): + return _make_client("llamacpp", model="m", base_url="http://localhost:9999") + + def _run_with_lines(self, client, lines, messages): + with patch( + "frigate.genai.plugins.llama_cpp.httpx.AsyncClient", + return_value=_FakeAsyncClient(lines), + ): + return _collect(client, messages) + + def test_stream_tool_call_arguments_are_dict(self): + lines = [ + "data: " + + json.dumps( + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "c1", + "function": { + "name": "search_objects", + "arguments": '{"label":', + }, + } + ] + } + } + ] + } + ), + "data: " + + json.dumps( + { + "choices": [ + { + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"arguments": ' "person"}'}, + } + ] + } + } + ] + } + ), + "data: " + + json.dumps({"choices": [{"delta": {}, "finish_reason": "tool_calls"}]}), + "data: [DONE]", + ] + client = self._client() + final = _final_message(self._run_with_lines(client, lines, SIMPLE_MESSAGES)) + self.assertEqual(final["finish_reason"], "tool_calls") + _assert_tool_args_are_dicts(final) + self.assertEqual(final["tool_calls"][0]["arguments"], {"label": "person"}) + + def test_stream_content_response(self): + lines = [ + "data: " + json.dumps({"choices": [{"delta": {"content": "hel"}}]}), + "data: " + json.dumps({"choices": [{"delta": {"content": "lo"}}]}), + "data: " + + json.dumps({"choices": [{"delta": {}, "finish_reason": "stop"}]}), + "data: [DONE]", + ] + client = self._client() + events = self._run_with_lines(client, lines, SIMPLE_MESSAGES) + deltas = [v for (k, v) in events if k == "content_delta"] + self.assertEqual("".join(deltas), "hello") + self.assertEqual(_final_message(events)["content"], "hello") + + def test_multimodal_message_does_not_crash(self): + lines = [ + "data: " + json.dumps({"choices": [{"delta": {"content": "ok"}}]}), + "data: " + + json.dumps({"choices": [{"delta": {}, "finish_reason": "stop"}]}), + "data: [DONE]", + ] + client = self._client() + final = _final_message(self._run_with_lines(client, lines, MULTIMODAL_MESSAGES)) + self.assertEqual(final["content"], "ok") + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_go2rtc_stream_auth.py b/frigate/test/test_go2rtc_stream_auth.py new file mode 100644 index 0000000000..b525c94ee1 --- /dev/null +++ b/frigate/test/test_go2rtc_stream_auth.py @@ -0,0 +1,175 @@ +"""Unit tests for `deny_response_for_go2rtc_stream`. + +Covers the camera-level authorization enforced in the `/auth` subrequest for +the nginx-proxied go2rtc live-stream paths (MSE/WebRTC WebSockets and the +WebRTC signaling endpoint). These paths name the stream via the `src` query +param, which the static-media auth in `media_auth` does not inspect. +""" + +import types +import unittest + +from frigate.api.auth import deny_response_for_go2rtc_stream +from frigate.config import FrigateConfig + +_CONFIG = { + "mqtt": {"host": "mqtt"}, + "auth": { + "roles": { + "limited_user": ["front_door"], + "dual_user": ["front_door", "back_door"], + } + }, + "cameras": { + "front_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + # go2rtc stream name differs from the camera name (substream) + "live": {"streams": {"Main Stream": "front_door_sub"}}, + }, + "back_door": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + "garage": { + "ffmpeg": { + "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}] + }, + "detect": {"height": 1080, "width": 1920, "fps": 5}, + }, + }, +} + + +def _request(config: FrigateConfig) -> types.SimpleNamespace: + return types.SimpleNamespace(app=types.SimpleNamespace(frigate_config=config)) + + +class TestDenyResponseForGo2rtcStream(unittest.TestCase): + def setUp(self) -> None: + self.config = FrigateConfig(**_CONFIG) + self.request = _request(self.config) + + def _deny(self, url: str, role: str): + return deny_response_for_go2rtc_stream(url, role, self.request) + + # --- non-stream paths pass through --- + + def test_non_stream_path_passes_through(self): + self.assertIsNone( + self._deny("http://host/clips/back_door-1.jpg", "limited_user") + ) + + def test_empty_url_passes_through(self): + self.assertIsNone(self._deny("", "limited_user")) + + def test_jsmpeg_path_not_handled_here(self): + # jsmpeg is authorized per-frame in the output pipeline, not here + self.assertIsNone( + self._deny("http://host/live/jsmpeg/back_door", "limited_user") + ) + + # --- restricted role: allowed vs forbidden cameras --- + + def test_mse_allowed_camera(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door", "limited_user") + ) + + def test_mse_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=back_door", "limited_user"), + 403, + ) + + def test_webrtc_ws_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/webrtc/api/ws?src=back_door", "limited_user"), + 403, + ) + + def test_webrtc_signaling_forbidden_camera_denied(self): + self.assertEqual( + self._deny("http://host/api/go2rtc/webrtc?src=back_door", "limited_user"), + 403, + ) + + def test_unknown_camera_denied(self): + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=nonexistent", "limited_user"), + 403, + ) + + def test_missing_src_denied(self): + self.assertEqual(self._deny("http://host/live/mse/api/ws", "limited_user"), 403) + + # --- multi-camera role: each assigned camera allowed, others denied --- + + def test_multi_camera_role_allows_first_assigned(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door", "dual_user") + ) + + def test_multi_camera_role_allows_second_assigned(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "dual_user") + ) + + def test_multi_camera_role_denies_unassigned(self): + # garage is configured but not in dual_user's allow-list + self.assertEqual( + self._deny("http://host/live/mse/api/ws?src=garage", "dual_user"), + 403, + ) + + # --- substream names resolve to their owning camera --- + + def test_allowed_substream_resolves_to_owning_camera(self): + # front_door_sub is owned by front_door, which limited_user may access + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=front_door_sub", "limited_user") + ) + + # --- multiple src values: deny if any is forbidden --- + + def test_multiple_src_one_forbidden_denied(self): + self.assertEqual( + self._deny( + "http://host/live/mse/api/ws?src=front_door&src=back_door", + "limited_user", + ), + 403, + ) + + def test_multiple_src_all_allowed(self): + self.assertIsNone( + self._deny( + "http://host/live/mse/api/ws?src=front_door&src=front_door_sub", + "limited_user", + ) + ) + + # --- privileged roles bypass the check --- + + def test_admin_bypasses(self): + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "admin") + ) + + def test_builtin_viewer_role_bypasses(self): + # the built-in viewer role is not in the config allow-list map, so it + # is treated as full access + self.assertIsNone( + self._deny("http://host/live/mse/api/ws?src=back_door", "viewer") + ) + + def test_missing_role_bypasses(self): + self.assertIsNone(self._deny("http://host/live/mse/api/ws?src=back_door", None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_gpu_stats.py b/frigate/test/test_gpu_stats.py index f6986912f7..c99414a419 100644 --- a/frigate/test/test_gpu_stats.py +++ b/frigate/test/test_gpu_stats.py @@ -21,8 +21,12 @@ class TestGpuStats(unittest.TestCase): @patch("frigate.util.services.time.sleep") @patch("frigate.util.services.time.monotonic") @patch("frigate.util.services._read_intel_drm_fdinfo") - def test_intel_gpu_stats_fdinfo(self, read_fdinfo, monotonic, sleep, get_names): + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_fdinfo( + self, drm_devices, read_fdinfo, monotonic, sleep, get_names + ): # 1 second of wall clock between snapshots + drm_devices.return_value = {"0000:00:02.0": "i915"} monotonic.side_effect = [0.0, 1.0] get_names.return_value = {"0000:00:02.0": "Intel Graphics"} @@ -40,18 +44,18 @@ class TestGpuStats(unittest.TestCase): "driver": "i915", "pid": "100", "engines": { - "render": (1_000_000_000, 0), - "video": (5_000_000_000, 0), - "video-enhance": (200_000_000, 0), - "compute": (0, 0), + "render": (1_000_000_000, 0, 1), + "video": (5_000_000_000, 0, 1), + "video-enhance": (200_000_000, 0, 1), + "compute": (0, 0, 1), }, }, ("0000:00:02.0", "2", "200"): { "driver": "i915", "pid": "200", "engines": { - "render": (0, 0), - "compute": (2_000_000_000, 0), + "render": (0, 0, 1), + "compute": (2_000_000_000, 0, 1), }, }, } @@ -60,18 +64,18 @@ class TestGpuStats(unittest.TestCase): "driver": "i915", "pid": "100", "engines": { - "render": (1_200_000_000, 0), - "video": (5_500_000_000, 0), - "video-enhance": (300_000_000, 0), - "compute": (0, 0), + "render": (1_200_000_000, 0, 1), + "video": (5_500_000_000, 0, 1), + "video-enhance": (300_000_000, 0, 1), + "compute": (0, 0, 1), }, }, ("0000:00:02.0", "2", "200"): { "driver": "i915", "pid": "200", "engines": { - "render": (0, 0), - "compute": (2_100_000_000, 0), + "render": (0, 0, 1), + "compute": (2_100_000_000, 0, 1), }, }, } @@ -92,7 +96,205 @@ class TestGpuStats(unittest.TestCase): }, } + @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services.time.monotonic") @patch("frigate.util.services._read_intel_drm_fdinfo") - def test_intel_gpu_stats_no_clients(self, read_fdinfo): + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_xe_capacity( + self, drm_devices, read_fdinfo, monotonic, sleep, get_names + ): + # Xe engines report cumulative cycles paired with total cycles, plus a + # per-class capacity. drm-cycles-* is summed across every instance of a + # class, so on Battlemage (capacity 2 for vcs/vecs) busy/total must be + # divided by capacity to land in 0-100%. + drm_devices.return_value = {"0000:03:00.0": "xe"} + monotonic.side_effect = [0.0, 1.0] + get_names.return_value = {"0000:03:00.0": "Intel Arc"} + + # Deltas over the window (busy, total): render 200/1000 cap 1 = 20%, + # video 800/1000 cap 2 = 40%, video-enhance 400/1000 cap 2 = 20%, + # compute 100/1000 cap 1 = 10%. Without the capacity divisor video/ + # video-enhance would read 80%/40% and dec would clamp at 100%. + snapshot_a = { + ("0000:03:00.0", "1", "300"): { + "driver": "xe", + "pid": "300", + "engines": { + "render": (0, 0, 1), + "video": (0, 0, 2), + "video-enhance": (0, 0, 2), + "compute": (0, 0, 1), + }, + }, + } + snapshot_b = { + ("0000:03:00.0", "1", "300"): { + "driver": "xe", + "pid": "300", + "engines": { + "render": (200, 1000, 1), + "video": (800, 1000, 2), + "video-enhance": (400, 1000, 2), + "compute": (100, 1000, 1), + }, + }, + } + read_fdinfo.side_effect = [snapshot_a, snapshot_b] + + intel_stats = get_intel_gpu_stats(None) + + assert intel_stats == { + "0000:03:00.0": { + "name": "Intel Arc", + "vendor": "intel", + "gpu": "90.0%", + "mem": "-%", + "compute": "30.0%", + "dec": "60.0%", + "clients": {"300": "90.0%"}, + }, + } + + @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_no_clients_reports_idle( + self, drm_devices, read_fdinfo, sleep, get_names + ): + # The device exists but nothing holds it open, e.g. while camera + # processes are restarting. This is an idle state, not an error: + # returning None here would latch the hwaccel error cooldown and + # blank GPU stats for an hour over a momentary gap. + drm_devices.return_value = {"0000:00:02.0": "i915"} read_fdinfo.return_value = {} + get_names.return_value = {"0000:00:02.0": "Intel Graphics"} + + assert get_intel_gpu_stats(None) == { + "0000:00:02.0": { + "name": "Intel Graphics", + "vendor": "intel", + "gpu": "0.0%", + "mem": "-%", + "compute": "0.0%", + "dec": "0.0%", + }, + } + # Idle short-circuits before spending the sample window + sleep.assert_not_called() + read_fdinfo.assert_called_once() + + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_clients_without_engine_counters( + self, drm_devices, read_fdinfo, sleep + ): + # i915 publishes drm-driver/drm-pdev/drm-client-id but no drm-engine-* + # lines while GuC submission is active on kernels older than 6.5, so + # clients are found with nothing to sample. Reporting idle here would + # be a lie, and sampling a second time cannot help. + drm_devices.return_value = {"0000:00:02.0": "i915"} + read_fdinfo.return_value = { + ("0000:00:02.0", "48", "1109"): { + "driver": "i915", + "pid": "1109", + "engines": {}, + }, + ("0000:00:02.0", "51", "1258"): { + "driver": "i915", + "pid": "1258", + "engines": {}, + }, + } + assert get_intel_gpu_stats(None) is None + sleep.assert_not_called() + read_fdinfo.assert_called_once() + + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_no_intel_device(self, drm_devices, read_fdinfo): + # Only a non-Intel GPU is visible in sysfs; /proc is never scanned + drm_devices.return_value = {"0000:01:00.0": "nvidia"} + + assert get_intel_gpu_stats(None) is None + read_fdinfo.assert_not_called() + + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + @patch("frigate.util.services._resolve_intel_gpu_pdev") + def test_intel_gpu_stats_unresolvable_device_hint( + self, resolve_pdev, drm_devices, read_fdinfo + ): + # A configured intel_gpu_device that cannot be resolved is a config + # error, not a reason to silently fall back to reporting all GPUs + resolve_pdev.return_value = None + + assert get_intel_gpu_stats("/dev/dri/renderD999") is None + drm_devices.assert_not_called() + read_fdinfo.assert_not_called() + + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + @patch("frigate.util.services._resolve_intel_gpu_pdev") + def test_intel_gpu_stats_hint_resolves_to_non_intel_gpu( + self, resolve_pdev, drm_devices, read_fdinfo + ): + # card numbering can reorder across reboots on multi-GPU hosts, so a + # configured hint may point at another vendor's card; call it out + # instead of reporting nothing + resolve_pdev.return_value = "0000:01:00.0" + drm_devices.return_value = { + "0000:00:02.0": "i915", + "0000:01:00.0": "nvidia", + } + + assert get_intel_gpu_stats("/dev/dri/card0") is None + read_fdinfo.assert_not_called() + + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_unreadable_proc(self, drm_devices, read_fdinfo): + # A scan failure (None) is a different condition than a scan that + # finds no clients ({}) and must not report idle + drm_devices.return_value = {"0000:00:02.0": "i915"} + read_fdinfo.return_value = None + + assert get_intel_gpu_stats(None) is None + + @patch("frigate.stats.intel_gpu_info.intel_gpu_name_resolver.get_names") + @patch("frigate.util.services.time.sleep") + @patch("frigate.util.services.time.monotonic") + @patch("frigate.util.services._read_intel_drm_fdinfo") + @patch("frigate.util.services._enumerate_drm_devices") + def test_intel_gpu_stats_clients_lost_between_samples( + self, drm_devices, read_fdinfo, monotonic, sleep, get_names + ): + # Clients disappearing during the sample window is transient process + # churn, so report idle rather than latching an error + drm_devices.return_value = {"0000:00:02.0": "i915"} + monotonic.side_effect = [0.0, 1.0] + get_names.return_value = {"0000:00:02.0": "Intel Graphics"} + read_fdinfo.side_effect = [ + { + ("0000:00:02.0", "1", "100"): { + "driver": "i915", + "pid": "100", + "engines": {"video": (5_000_000_000, 0, 1)}, + }, + }, + {}, + ] + + assert get_intel_gpu_stats(None) == { + "0000:00:02.0": { + "name": "Intel Graphics", + "vendor": "intel", + "gpu": "0.0%", + "mem": "-%", + "compute": "0.0%", + "dec": "0.0%", + }, + } diff --git a/frigate/test/test_keyframe_analysis.py b/frigate/test/test_keyframe_analysis.py new file mode 100644 index 0000000000..14dad5849d --- /dev/null +++ b/frigate/test/test_keyframe_analysis.py @@ -0,0 +1,110 @@ +"""Tests for keyframe-spacing analysis used to detect smart/+ codecs.""" + +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from frigate.util.services import ( + analyze_record_keyframes, + classify_keyframe_gaps, + parse_keyframe_packets, +) + + +class TestClassifyKeyframeGaps(unittest.TestCase): + def test_ok_when_gaps_small(self): + # keyframes every ~1s + pts = [0.0, 1.0, 2.0, 3.0, 4.0] + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "ok") + self.assertEqual(result["max_gap"], 1.0) + self.assertEqual(result["keyframe_count"], 5) + self.assertEqual(result["thresholds"], {"warning": 4.0, "error": 10}) + + def test_warning_when_gap_exceeds_four_seconds(self): + pts = [0.0, 1.0, 6.5] # 5.5s gap + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "warning") + self.assertEqual(result["max_gap"], 5.5) + + def test_error_when_gap_exceeds_segment_time(self): + pts = [0.0, 12.0] # 12s gap > 10s segment + result = classify_keyframe_gaps(pts, segment_time=10) + self.assertEqual(result["severity"], "error") + + def test_error_threshold_tracks_segment_time(self): + pts = [0.0, 6.0] # 6s gap, segment_time=5 -> error + result = classify_keyframe_gaps(pts, segment_time=5) + self.assertEqual(result["severity"], "error") + + def test_unknown_with_single_keyframe(self): + result = classify_keyframe_gaps([1.0], segment_time=10) + self.assertEqual(result["severity"], "unknown") + self.assertIsNone(result["max_gap"]) + self.assertEqual(result["keyframe_count"], 1) + + def test_unknown_with_no_keyframes(self): + result = classify_keyframe_gaps([], segment_time=10) + self.assertEqual(result["severity"], "unknown") + self.assertEqual(result["keyframe_count"], 0) + + +class TestParseKeyframePackets(unittest.TestCase): + def test_extracts_keyframe_pts_and_max(self): + output = "0.000000,K__\n0.033333,___\n1.000000,K__\n1.500000,___\n" + keyframe_pts, max_pts = parse_keyframe_packets(output) + self.assertEqual(keyframe_pts, [0.0, 1.0]) + self.assertEqual(max_pts, 1.5) + + def test_skips_unparseable_and_empty_lines(self): + output = "N/A,K__\n\n2.0,K__\nbad line\n" + keyframe_pts, max_pts = parse_keyframe_packets(output) + self.assertEqual(keyframe_pts, [2.0]) + self.assertEqual(max_pts, 2.0) + + def test_empty_output(self): + keyframe_pts, max_pts = parse_keyframe_packets("") + self.assertEqual(keyframe_pts, []) + self.assertIsNone(max_pts) + + +class TestAnalyzeRecordKeyframes(unittest.IsolatedAsyncioTestCase): + async def test_merges_duration_and_classification(self): + csv = b"0.0,K__\n1.0,___\n6.0,K__\n7.0,___\n" + proc = MagicMock() + proc.communicate = AsyncMock(return_value=(csv, b"")) + ffmpeg = MagicMock() + ffmpeg.ffprobe_path = "/usr/bin/ffprobe" + + with patch( + "frigate.util.services.asyncio.create_subprocess_exec", + AsyncMock(return_value=proc), + ): + result = await analyze_record_keyframes( + ffmpeg, "rtsp://cam/stream", segment_time=10 + ) + + self.assertEqual(result["severity"], "warning") # 6s gap > 4s + self.assertEqual(result["max_gap"], 6.0) + self.assertEqual(result["duration_observed"], 7.0) + + async def test_timeout_returns_unknown(self): + proc = MagicMock() + proc.communicate = AsyncMock(side_effect=TimeoutError()) + proc.kill = MagicMock() + ffmpeg = MagicMock() + ffmpeg.ffprobe_path = "/usr/bin/ffprobe" + + with patch( + "frigate.util.services.asyncio.create_subprocess_exec", + AsyncMock(return_value=proc), + ): + result = await analyze_record_keyframes( + ffmpeg, "rtsp://cam/stream", segment_time=10 + ) + + self.assertEqual(result["severity"], "unknown") + proc.kill.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py index 3ac4d8a071..715cd5a1a1 100644 --- a/frigate/test/test_maintainer.py +++ b/frigate/test/test_maintainer.py @@ -93,7 +93,7 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase): stop_event = MagicMock() maintainer = RecordingMaintainer(config, stop_event) - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) start_time = now - datetime.timedelta(seconds=20) end_time = now - datetime.timedelta(seconds=10) cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4" @@ -115,6 +115,46 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase): self.assertIsNone(result) maintainer.drop_segment.assert_called_once_with(cache_path) + async def test_expire_stale_recordings_info_drops_only_absent_cameras(self): + config = MagicMock(spec=FrigateConfig) + config.cameras = {} + stop_event = MagicMock() + maintainer = RecordingMaintainer(config, stop_event) + + now = datetime.datetime.now().timestamp() + ancient = now - 86400 + recent = now - 1 + + maintainer.object_recordings_info["present_cam"] = [(ancient, [], [], [])] + maintainer.audio_recordings_info["present_cam"] = [(ancient, 0, [])] + + maintainer.object_recordings_info["absent_cam"] = [ + (ancient, [], [], []), + (recent, [], [], []), + ] + maintainer.audio_recordings_info["absent_cam"] = [ + (ancient, 0, []), + (recent, 0, []), + ] + + grouped_recordings = {"present_cam": [{"start_time": ancient}]} + + maintainer._expire_stale_recordings_info(grouped_recordings) + + self.assertEqual( + maintainer.object_recordings_info["present_cam"], [(ancient, [], [], [])] + ) + self.assertEqual( + maintainer.audio_recordings_info["present_cam"], [(ancient, 0, [])] + ) + + self.assertEqual( + maintainer.object_recordings_info["absent_cam"], [(recent, [], [], [])] + ) + self.assertEqual( + maintainer.audio_recordings_info["absent_cam"], [(recent, 0, [])] + ) + if __name__ == "__main__": unittest.main() diff --git a/frigate/test/test_mqtt_topic_registration.py b/frigate/test/test_mqtt_topic_registration.py new file mode 100644 index 0000000000..309fafb983 --- /dev/null +++ b/frigate/test/test_mqtt_topic_registration.py @@ -0,0 +1,100 @@ +"""Tests for MQTT command topic callback registration.""" + +import unittest +from unittest.mock import MagicMock, patch + +from frigate.comms.mqtt import MqttClient + + +def _make_camera_mock( + *, + enabled: bool = True, + notifications_enabled_in_config: bool = False, +) -> MagicMock: + """Build a camera config mock with the fields _start() reads.""" + camera = MagicMock() + camera.enabled = enabled + camera.notifications.enabled_in_config = notifications_enabled_in_config + camera.onvif.host = None + camera.motion.mask = {} + camera.objects.mask = {} + camera.zones = {} + return camera + + +def _registered_topics( + cameras: dict[str, MagicMock], + *, + global_notifications_enabled_in_config: bool = False, +) -> set[str]: + """Start an MqttClient against a mocked paho client and collect the + topics registered via message_callback_add.""" + config = MagicMock() + config.cameras = cameras + config.notifications.enabled_in_config = global_notifications_enabled_in_config + config.mqtt.topic_prefix = "frigate" + config.mqtt.client_id = "frigate" + config.mqtt.user = None + config.mqtt.tls_ca_certs = None + config.mqtt.tls_insecure = None + + with patch("frigate.comms.mqtt.mqtt.Client") as client_cls: + mqtt_client = MqttClient(config) + mqtt_client.subscribe(MagicMock()) + + paho_client = client_cls.return_value + return {call.args[0] for call in paho_client.message_callback_add.call_args_list} + + +class TestMqttTopicRegistration(unittest.TestCase): + def test_camera_notification_topics_registered(self): + """Per-camera notification set/suspend must be registered so paho + routes them to the dispatcher (unregistered topics drop silently).""" + topics = _registered_topics( + {"front_door": _make_camera_mock(notifications_enabled_in_config=True)} + ) + + self.assertIn("frigate/front_door/notifications/set", topics) + self.assertIn("frigate/front_door/notifications/suspend", topics) + + def test_global_set_registered_with_camera_only_notifications(self): + """The global topic must work when notifications are enabled only at + the camera level, matching the WebPushClient gating in app.py.""" + topics = _registered_topics( + {"front_door": _make_camera_mock(notifications_enabled_in_config=True)}, + global_notifications_enabled_in_config=False, + ) + + self.assertIn("frigate/notifications/set", topics) + + def test_global_set_registered_with_global_notifications(self): + topics = _registered_topics( + {"front_door": _make_camera_mock()}, + global_notifications_enabled_in_config=True, + ) + + self.assertIn("frigate/notifications/set", topics) + + def test_global_set_not_registered_when_notifications_unconfigured(self): + topics = _registered_topics( + {"front_door": _make_camera_mock()}, + global_notifications_enabled_in_config=False, + ) + + self.assertNotIn("frigate/notifications/set", topics) + + def test_disabled_camera_does_not_enable_global_set(self): + topics = _registered_topics( + { + "front_door": _make_camera_mock( + enabled=False, notifications_enabled_in_config=True + ) + }, + global_notifications_enabled_in_config=False, + ) + + self.assertNotIn("frigate/notifications/set", topics) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_norfair_distance.py b/frigate/test/test_norfair_distance.py new file mode 100644 index 0000000000..a79b91c3cf --- /dev/null +++ b/frigate/test/test_norfair_distance.py @@ -0,0 +1,91 @@ +import math +import unittest + +import numpy as np +from norfair.camera_motion import ( + HomographyTransformation, + TranslationTransformation, +) + +from frigate.ptz.autotrack import transform_is_finite +from frigate.track.norfair_tracker import distance + + +class TestNorfairDistance(unittest.TestCase): + """Regression tests for the tracker distance guard. + + norfair raises a hard ValueError on any nan distance, which kills the camera + process. During autotracking, an ill-conditioned homography can hand the + tracker a non-finite or degenerate estimate box, so distance() must never + return nan for any input. + """ + + def setUp(self) -> None: + # boxes are [[x1, y1], [x2, y2]] + self.detection = np.array([[805.0, 402.0], [864.0, 521.0]]) + self.estimate = np.array([[800.0, 400.0], [860.0, 520.0]]) + + def test_finite_boxes_give_finite_distance(self) -> None: + d = distance(self.detection, self.estimate) + self.assertTrue(math.isfinite(d)) + + def test_inf_estimate_corner_does_not_return_nan(self) -> None: + estimate = np.array([[np.inf, 400.0], [860.0, 520.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_nan_estimate_corner_does_not_return_nan(self) -> None: + # the actual autotracking crash: a positive-only guard would miss this + # because nan <= 0 is False + estimate = np.array([[np.nan, 400.0], [860.0, 520.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_zero_area_estimate_does_not_return_nan(self) -> None: + estimate = np.array([[900.0, 500.0], [900.0, 500.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_zero_area_detection_does_not_return_nan(self) -> None: + detection = np.array([[805.0, 402.0], [805.0, 521.0]]) + d = distance(detection, self.estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + def test_inverted_estimate_corners_do_not_return_nan(self) -> None: + # Kalman estimates can occasionally cross corners (x2 < x1) + estimate = np.array([[860.0, 520.0], [800.0, 400.0]]) + d = distance(self.detection, estimate) + self.assertFalse(math.isnan(d)) + self.assertEqual(d, float("inf")) + + +class TestTransformIsFinite(unittest.TestCase): + def test_finite_homography_is_finite(self) -> None: + matrix = np.array([[1.0, 0.0, 5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]]) + self.assertTrue(transform_is_finite(HomographyTransformation(matrix))) + + def test_finite_translation_is_finite(self) -> None: + self.assertTrue( + transform_is_finite(TranslationTransformation(np.array([12.0, -4.0]))) + ) + + def test_non_finite_homography_is_not_finite(self) -> None: + transform = HomographyTransformation(np.eye(3)) + # simulate accumulation overflowing to a non-finite matrix + transform.homography_matrix = np.array( + [[1.0, 0.0, np.inf], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] + ) + self.assertFalse(transform_is_finite(transform)) + + def test_nan_translation_is_not_finite(self) -> None: + self.assertFalse( + transform_is_finite(TranslationTransformation(np.array([np.nan, 0.0]))) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_obects.py b/frigate/test/test_obects.py index 8fe831980e..ee0162ec01 100644 --- a/frigate/test/test_obects.py +++ b/frigate/test/test_obects.py @@ -1,6 +1,22 @@ +import random import unittest +import numpy as np + from frigate.track.tracked_object import TrackedObjectAttribute +from frigate.util.object import average_boxes + + +class TestBoxStatistics(unittest.TestCase): + def test_average_boxes_matches_numpy(self) -> None: + rng = random.Random(0) + for _ in range(5000): + boxes = [ + [rng.randint(0, 4000) for _ in range(4)] + for _ in range(rng.randint(1, 10)) + ] + expected = [float(np.mean([b[i] for b in boxes])) for i in range(4)] + self.assertEqual(average_boxes(boxes), expected) class TestAttribute(unittest.TestCase): diff --git a/frigate/test/test_ptz_autotrack.py b/frigate/test/test_ptz_autotrack.py new file mode 100644 index 0000000000..1abcc780d0 --- /dev/null +++ b/frigate/test/test_ptz_autotrack.py @@ -0,0 +1,130 @@ +"""Tests for autotracker state that must survive runtime config changes. + +Regression coverage for a family of bugs where per-camera autotracker state was +built once at startup and never revisited. A camera that is added or enabled +after startup, or has autotracking enabled from the UI, would either raise a +KeyError on the autotracker thread or silently keep the wrong state: + +- autotracker_init only got an entry for cameras enabled when PtzAutoTracker was + constructed, so runtime-enabled cameras raised KeyError on lookup. +- ptz_metrics autotracker_enabled is what the camera processes read, but nothing + updated it when autotracking was enabled through a config save, so it stayed + False and the tracker never built a motion estimator. +""" + +import unittest +from unittest.mock import MagicMock + +from frigate.camera import PTZMetrics +from frigate.config import FrigateConfig +from frigate.ptz.autotrack import PtzAutoTracker + +CAMERA = "ptz_cam" + + +def _config(autotracking_enabled: bool) -> FrigateConfig: + return FrigateConfig( + **{ + "mqtt": {"enabled": False}, + "cameras": { + CAMERA: { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"width": 1920, "height": 1080}, + "zones": {"zone": {"coordinates": "0,0,1,0,1,1,0,1"}}, + "onvif": { + "host": "10.0.0.1", + "autotracking": { + "enabled": autotracking_enabled, + "required_zones": ["zone"], + }, + }, + } + }, + } + ) + + +def _make_tracker(autotracking_enabled: bool = True) -> PtzAutoTracker: + """Build a PtzAutoTracker without invoking __init__, which would try to set up + onvif over the network. Only the config/metrics state is relevant here.""" + tracker = PtzAutoTracker.__new__(PtzAutoTracker) + tracker.config = _config(autotracking_enabled) + tracker.ptz_metrics = {CAMERA: PTZMetrics(autotracker_enabled=False)} + tracker.onvif = MagicMock() + tracker.config_subscriber = MagicMock() + tracker.autotracker_init = {} + tracker.calibrating = {} + tracker.tracked_object = {} + return tracker + + +class TestAutotrackerInitGuards(unittest.IsolatedAsyncioTestCase): + async def test_camera_maintenance_returns_early_when_not_initialized(self) -> None: + # a camera enabled at runtime has no autotracker_init entry, which used to + # raise KeyError and kill the autotracker thread for every camera + tracker = _make_tracker() + self.assertNotIn(CAMERA, tracker.autotracker_init) + + await tracker.camera_maintenance(CAMERA) + + tracker.onvif.get_camera_status.assert_not_called() + + async def test_camera_maintenance_returns_early_when_init_incomplete(self) -> None: + # autotracker_init is seeded False for enabled cameras before setup runs + tracker = _make_tracker() + tracker.autotracker_init[CAMERA] = False + + await tracker.camera_maintenance(CAMERA) + + tracker.onvif.get_camera_status.assert_not_called() + + +class TestAutotrackerMetricSync(unittest.TestCase): + def test_metric_follows_config_when_enabled_by_update(self) -> None: + # autotracking enabled via a config save: the metric was seeded False when + # the camera was added and nothing else updates it + tracker = _make_tracker(autotracking_enabled=True) + metrics = tracker.ptz_metrics[CAMERA] + self.assertFalse(metrics.autotracker_enabled.value) + + tracker.config_subscriber.check_for_updates.return_value = {"onvif": [CAMERA]} + tracker.check_for_updates() + + self.assertTrue(metrics.autotracker_enabled.value) + + def test_metric_follows_config_when_disabled_by_update(self) -> None: + tracker = _make_tracker(autotracking_enabled=False) + metrics = tracker.ptz_metrics[CAMERA] + metrics.autotracker_enabled.value = True + + tracker.config_subscriber.check_for_updates.return_value = { + "autotracking": [CAMERA] + } + tracker.check_for_updates() + + self.assertFalse(metrics.autotracker_enabled.value) + + def test_metric_sync_skips_camera_without_metrics(self) -> None: + # `add` reaches the maintainer and the autotracker on separate threads with + # no ordering guarantee, so the metrics may not exist yet + tracker = _make_tracker() + tracker.ptz_metrics = {} + tracker.config_subscriber.check_for_updates.return_value = {"add": [CAMERA]} + + tracker.check_for_updates() + + def test_metric_sync_skips_unknown_camera(self) -> None: + tracker = _make_tracker() + tracker.config_subscriber.check_for_updates.return_value = { + "add": ["not_in_config"] + } + + tracker.check_for_updates() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_ptz_onvif.py b/frigate/test/test_ptz_onvif.py new file mode 100644 index 0000000000..d2dd2d046e --- /dev/null +++ b/frigate/test/test_ptz_onvif.py @@ -0,0 +1,147 @@ +"""Tests for ONVIF init state that must not depend on the autotracking config. + +Regression coverage for a camera that is initialized while autotracking is off and +has it enabled later, which is the normal wizard flow: set the camera up first, +configure autotracking afterwards. The autotracking-only request objects used to +be created only when autotracking was enabled at init time, so the camera was left +with init=True but no status_request. get_camera_status skips its re-init branch +when init is True, so it went straight to the missing key and raised KeyError on +the tracking thread. + +The request objects are built from the locally parsed WSDL and cost no network, so +they are always created and init=True now implies they exist. +""" + +import unittest +from unittest.mock import AsyncMock, MagicMock + +from frigate.config import FrigateConfig +from frigate.ptz.onvif import OnvifController + +CAMERA = "ptz_cam" + + +def _config(autotracking_enabled: bool) -> FrigateConfig: + return FrigateConfig( + **{ + "mqtt": {"enabled": False}, + "cameras": { + CAMERA: { + "ffmpeg": { + "inputs": [ + {"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]} + ] + }, + "detect": {"width": 1920, "height": 1080}, + "zones": {"zone": {"coordinates": "0,0,1,0,1,1,0,1"}}, + "onvif": { + "host": "10.0.0.1", + "autotracking": { + "enabled": autotracking_enabled, + "required_zones": ["zone"], + }, + }, + } + }, + } + ) + + +def _make_profile() -> MagicMock: + profile = MagicMock() + profile.token = "profile_1" + profile.Name = "MainStream" + profile.VideoEncoderConfiguration = MagicMock() + ptz_config = MagicMock() + ptz_config.token = "ptz_config_1" + ptz_config.DefaultContinuousPanTiltVelocitySpace = "space" + ptz_config.DefaultContinuousZoomVelocitySpace = "space" + profile.PTZConfiguration = ptz_config + return profile + + +def _make_onvif_camera() -> MagicMock: + """A camera that supports PTZ but nothing optional, so init takes the simplest + path through the feature detection below.""" + onvif = MagicMock() + onvif.update_xaddrs = AsyncMock() + + video_source = MagicMock() + video_source.token = "video_source_1" + + media = MagicMock() + media.GetProfiles = AsyncMock(return_value=[_make_profile()]) + media.GetVideoSources = AsyncMock(return_value=[video_source]) + onvif.create_media_service = AsyncMock(return_value=media) + onvif.get_definition = MagicMock(return_value={"ptz": "definition"}) + + ptz = MagicMock() + # create_type is a local WSDL lookup, so tag the result to assert on it later + ptz.create_type = MagicMock(side_effect=lambda name: MagicMock(request_type=name)) + ptz.GetConfigurationOptions = AsyncMock(side_effect=Exception("not supported")) + onvif.create_ptz_service = AsyncMock(return_value=ptz) + onvif.create_imaging_service = AsyncMock(side_effect=Exception("not supported")) + return onvif + + +def _make_controller(autotracking_enabled: bool) -> OnvifController: + """Build a controller without invoking __init__, which would start an event loop + thread and reach out to the camera.""" + config = _config(autotracking_enabled) + controller = OnvifController.__new__(OnvifController) + controller.config = config + controller.cams = {CAMERA: {"onvif": _make_onvif_camera(), "init": False}} + controller.failed_cams = {} + controller.camera_configs = {CAMERA: config.cameras[CAMERA]} + controller.ptz_metrics = {CAMERA: MagicMock()} + return controller + + +class TestOnvifInitRequests(unittest.IsolatedAsyncioTestCase): + async def test_status_request_created_when_autotracking_disabled(self) -> None: + # the wizard flow: onvif configured first, autotracking enabled later + controller = _make_controller(autotracking_enabled=False) + + self.assertTrue(await controller._init_onvif(CAMERA)) + + cam = controller.cams[CAMERA] + self.assertTrue(cam["init"]) + self.assertIn("status_request", cam) + self.assertIn("service_capabilities_request", cam) + + async def test_status_request_created_when_autotracking_enabled(self) -> None: + controller = _make_controller(autotracking_enabled=True) + + self.assertTrue(await controller._init_onvif(CAMERA)) + + cam = controller.cams[CAMERA] + self.assertIn("status_request", cam) + self.assertIn("service_capabilities_request", cam) + + async def test_init_implies_status_request_exists(self) -> None: + # the invariant get_camera_status relies on: it skips re-init when init is + # True and then reads status_request without guarding + for autotracking_enabled in (True, False): + with self.subTest(autotracking_enabled=autotracking_enabled): + controller = _make_controller(autotracking_enabled) + + await controller._init_onvif(CAMERA) + + cam = controller.cams[CAMERA] + if cam["init"]: + self.assertEqual(cam["status_request"].request_type, "GetStatus") + + async def test_requests_built_without_contacting_camera(self) -> None: + # create_type is a local WSDL lookup; cameras that do not implement + # GetServiceCapabilities must not be asked about it during init + controller = _make_controller(autotracking_enabled=False) + + await controller._init_onvif(CAMERA) + + ptz = controller.cams[CAMERA]["ptz"] + ptz.GetServiceCapabilities.assert_not_called() + ptz.GetStatus.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_sqlitevecq_regexp.py b/frigate/test/test_sqlitevecq_regexp.py new file mode 100644 index 0000000000..71f4fcb6b4 --- /dev/null +++ b/frigate/test/test_sqlitevecq_regexp.py @@ -0,0 +1,54 @@ +"""Tests for the REGEXP function registered on the main Frigate database. + +Regression coverage for GHSA-q8jx-q884-jcq9: an attacker-controlled +catastrophic (ReDoS) pattern reaching the REGEXP sink must not be able to +stall the serialized database worker thread. +""" + +import sqlite3 +import time +import unittest + +from frigate.db.sqlitevecq import REGEXP_TIMEOUT_SECONDS, SqliteVecQueueDatabase + + +class TestRegexpFunction(unittest.TestCase): + def setUp(self) -> None: + # autostart=False keeps the queue worker thread from spinning up; we + # only need the REGEXP registration, exercised on our own connection. + self.db = SqliteVecQueueDatabase(":memory:", autostart=False) + self.conn = sqlite3.connect(":memory:") + self.db._register_regexp(self.conn) + + def tearDown(self) -> None: + self.conn.close() + + def _regexp(self, value: str | None, pattern: str) -> int | None: + # SQLite maps "value REGEXP pattern" to regexp(pattern, value). + return self.conn.execute("SELECT ? REGEXP ?", (value, pattern)).fetchone()[0] + + def test_normal_patterns_still_match(self) -> None: + self.assertTrue(self._regexp("ABC123", "^ABC")) + self.assertTrue(self._regexp("ABC123", "ABC.*")) + self.assertTrue(self._regexp("ABC123", "[0-9]+$")) + self.assertFalse(self._regexp("ABC123", "^XYZ")) + + def test_null_value_does_not_match(self) -> None: + self.assertFalse(self._regexp(None, ".*")) + + def test_invalid_pattern_does_not_raise(self) -> None: + self.assertFalse(self._regexp("ABC123", "(unclosed")) + + def test_catastrophic_pattern_is_time_bounded(self) -> None: + # Without the timeout this evaluation backtracks for minutes to hours + # and wedges the whole database thread (GHSA-q8jx-q884-jcq9). + catastrophic = "(a{2,})+c" + subject = "a" * 4000 + + start = time.monotonic() + result = self._regexp(subject, catastrophic) + elapsed = time.monotonic() - start + + # The pattern does not match; the guarantee is that it returns quickly. + self.assertFalse(result) + self.assertLess(elapsed, REGEXP_TIMEOUT_SECONDS + 2.0) diff --git a/frigate/test/test_stationary_classifier.py b/frigate/test/test_stationary_classifier.py new file mode 100644 index 0000000000..bd5e2c41e2 --- /dev/null +++ b/frigate/test/test_stationary_classifier.py @@ -0,0 +1,39 @@ +"""Tests for stationary object classification thresholds.""" + +import unittest + +from frigate.track.stationary_classifier import ( + DEFAULT_OBJECT_THRESHOLDS, + DYNAMIC_OBJECT_THRESHOLDS, + NON_STATIONARY_OBJECT_THRESHOLDS, + STATIONARY_OBJECT_THRESHOLDS, + StationaryThresholds, + get_stationary_threshold, +) + + +class TestStationaryThresholds(unittest.TestCase): + def test_known_labels_return_expected_singletons(self) -> None: + self.assertIs(get_stationary_threshold("package"), STATIONARY_OBJECT_THRESHOLDS) + self.assertIs(get_stationary_threshold("car"), DYNAMIC_OBJECT_THRESHOLDS) + self.assertIs( + get_stationary_threshold("license_plate"), + NON_STATIONARY_OBJECT_THRESHOLDS, + ) + + def test_unknown_label_returns_shared_default(self) -> None: + # an unknown label must reuse the shared default instance, not allocate + # a fresh one on every call (this runs per object per frame) + first = get_stationary_threshold("person") + second = get_stationary_threshold("dog") + self.assertIs(first, DEFAULT_OBJECT_THRESHOLDS) + self.assertIs(second, DEFAULT_OBJECT_THRESHOLDS) + + def test_default_matches_a_fresh_instance(self) -> None: + # the shared default must be value-equivalent to the previous + # per-call StationaryThresholds() + self.assertEqual(DEFAULT_OBJECT_THRESHOLDS, StationaryThresholds()) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/test/test_update_yaml.py b/frigate/test/test_update_yaml.py new file mode 100644 index 0000000000..e9e160c8f4 --- /dev/null +++ b/frigate/test/test_update_yaml.py @@ -0,0 +1,183 @@ +"""Test in-place yaml config updates.""" + +import os +import tempfile +import unittest + +from ruamel.yaml import YAML + +from frigate.util.builtin import update_yaml_file_bulk + + +class TestUpdateYaml(unittest.TestCase): + def setUp(self) -> None: + self.yaml = YAML() + fd, self.config_path = tempfile.mkstemp(suffix=".yml") + os.close(fd) + + def tearDown(self) -> None: + os.unlink(self.config_path) + + def _write(self, text: str) -> None: + with open(self.config_path, "w") as f: + f.write(text) + + def _read(self) -> str: + with open(self.config_path) as f: + return f.read() + + def _load(self): + with open(self.config_path) as f: + return self.yaml.load(f) + + def test_delete_key(self): + """Deleting a key removes it and leaves valid yaml.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " mask: 0,0.45,0.245,0.45\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + data = self._load() + assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"] + + def test_delete_commented_key_emptying_map(self): + """Deleting the only key of a map whose key carries comments must not + emit unparseable yaml (orphaned comment tokens above a flow-style {}).""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # cars parked across the street\n" + " # second comment line\n" + " mask: 0,0.45,0.245,0.45\n" + " motion:\n" + " mask: 0,0.449,0.686,0.395\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + # must re-parse cleanly + data = self._load() + assert "mask" not in data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert data["cameras"]["cam1"]["motion"]["mask"] == "0,0.449,0.686,0.395" + # the orphaned comments must be gone from the file, not just parseable + content = self._read() + assert "cars parked across the street" not in content + assert "second comment line" not in content + + def test_delete_last_named_mask_emptying_map(self): + """The path the current UI actually sends: a named object mask deleted + down to an empty `mask` map, with a comment inside that map.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " mask:\n" + " # ignore the neighbor's driveway\n" + " driveway:\n" + " coordinates: 0,0.1,0.2,0.3\n" + ) + update_yaml_file_bulk( + self.config_path, + {"cameras.cam1.objects.filters.car.mask.driveway": ""}, + ) + data = self._load() + assert data["cameras"]["cam1"]["objects"]["filters"]["car"]["mask"] == {} + assert "ignore the neighbor's driveway" not in self._read() + + def test_delete_last_commented_list_item(self): + """Deleting the last element of a commented sequence must not emit + an orphaned comment above a flow-style [] at column 0.""" + self._write( + "cameras:\n" + " cam1:\n" + " motion:\n" + " mask:\n" + " # driveway motion mask\n" + " - 0,0.4,0.6,0.4\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""}) + data = self._load() + assert data["cameras"]["cam1"]["motion"]["mask"] == [] + assert "driveway motion mask" not in self._read() + + def test_delete_list_item_preserves_remaining(self): + """Deleting one element of a sequence keeps the others and stays valid.""" + self._write( + "cameras:\n" + " cam1:\n" + " motion:\n" + " mask:\n" + " - 0,0.4,0.6,0.4\n" + " - 0,0.1,0.2,0.3\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.motion.mask.0": ""}) + data = self._load() + assert data["cameras"]["cam1"]["motion"]["mask"] == ["0,0.1,0.2,0.3"] + + def test_delete_key_preserves_siblings(self): + """Deleting one key among several keeps the sibling entries and any + comments on keys preceding the deleted one.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # mask drawn around the parked suv\n" + " mask: 0,0.45,0.245,0.45\n" + " threshold: 0.8\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.threshold": ""} + ) + data = self._load() + car = data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert "threshold" not in car + assert car["mask"] == "0,0.45,0.245,0.45" + assert "# mask drawn around the parked suv" in self._read() + + def test_delete_first_commented_key_keeps_map_valid(self): + """Deleting a commented key from a map that still has other keys + leaves the remaining entries intact and the file parseable.""" + self._write( + "cameras:\n" + " cam1:\n" + " objects:\n" + " filters:\n" + " car:\n" + " # comment on the deleted key\n" + " mask: 0,0.45,0.245,0.45\n" + " threshold: 0.8\n" + ) + update_yaml_file_bulk( + self.config_path, {"cameras.cam1.objects.filters.car.mask": ""} + ) + data = self._load() + car = data["cameras"]["cam1"]["objects"]["filters"]["car"] + assert "mask" not in car + assert car["threshold"] == 0.8 + + def test_update_value_preserves_comments(self): + """Updating a value keeps surrounding comments intact.""" + self._write( + "cameras:\n cam1:\n detect:\n # tuned for the pi\n fps: 4\n" + ) + update_yaml_file_bulk(self.config_path, {"cameras.cam1.detect.fps": 5}) + data = self._load() + assert data["cameras"]["cam1"]["detect"]["fps"] == 5 + assert "# tuned for the pi" in self._read() + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/frigate/test/test_video.py b/frigate/test/test_video.py index 8612990e2e..d09a60559f 100644 --- a/frigate/test/test_video.py +++ b/frigate/test/test_video.py @@ -82,6 +82,24 @@ class TestRegion(unittest.TestCase): assert len(cluster_candidates) == 2 + def test_cluster_candidates_partition_boxes(self): + # every box index must appear in exactly one cluster (no box used twice, + # none dropped) - the invariant the used-box tracking enforces + boxes = [ + (100, 100, 200, 200), + (202, 150, 252, 200), + (210, 160, 260, 210), + (900, 900, 950, 950), + (905, 905, 955, 955), + ] + + cluster_candidates = get_cluster_candidates( + self.frame_shape, self.min_region_size, boxes + ) + + assigned = [idx for cluster in cluster_candidates for idx in cluster] + self.assertEqual(sorted(assigned), list(range(len(boxes)))) + def test_transliterate_to_latin(self): self.assertEqual(transliterate_to_latin("frégate"), "fregate") self.assertEqual(transliterate_to_latin("utilité"), "utilite") diff --git a/frigate/test/test_webpush_suspend.py b/frigate/test/test_webpush_suspend.py new file mode 100644 index 0000000000..1442bad21d --- /dev/null +++ b/frigate/test/test_webpush_suspend.py @@ -0,0 +1,64 @@ +"""Tests for notification suspension expiry handling.""" + +import datetime +import unittest +from unittest.mock import MagicMock + +from frigate.comms.webpush import WebPushClient + + +class TestSuspensionExpiry(unittest.TestCase): + def _make_client(self, suspended_cameras: dict[str, int]) -> WebPushClient: + client = WebPushClient.__new__(WebPushClient) + client.suspended_cameras = suspended_cameras + client.suspension_broadcaster = MagicMock() + return client + + def test_clears_and_broadcasts_expired_suspension(self): + now = datetime.datetime.now().timestamp() + client = self._make_client({"front_door": int(now - 60)}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], 0) + client.suspension_broadcaster.assert_called_once_with( + "front_door/notifications/suspended", "0", True + ) + + def test_leaves_active_suspension_untouched(self): + now = datetime.datetime.now().timestamp() + suspend_until = int(now + 3600) + client = self._make_client({"front_door": suspend_until}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], suspend_until) + client.suspension_broadcaster.assert_not_called() + + def test_ignores_unsuspended_camera(self): + client = self._make_client({"front_door": 0}) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["front_door"], 0) + client.suspension_broadcaster.assert_not_called() + + def test_only_expired_cameras_are_cleared(self): + now = datetime.datetime.now().timestamp() + active_until = int(now + 3600) + client = self._make_client( + { + "expired": int(now - 5), + "active": active_until, + "idle": 0, + } + ) + + client._clear_expired_suspensions() + + self.assertEqual(client.suspended_cameras["expired"], 0) + self.assertEqual(client.suspended_cameras["active"], active_until) + self.assertEqual(client.suspended_cameras["idle"], 0) + client.suspension_broadcaster.assert_called_once_with( + "expired/notifications/suspended", "0", True + ) diff --git a/frigate/test/test_ws_auth.py b/frigate/test/test_ws_auth.py index b762f4384c..a9fc6e1320 100644 --- a/frigate/test/test_ws_auth.py +++ b/frigate/test/test_ws_auth.py @@ -11,6 +11,16 @@ class TestCheckWsAuthorization(unittest.TestCase): DEFAULT_SEPARATOR = "," + # admin/viewer are reserved and always map to all cameras (empty list); + # custom roles map to a specific set of cameras. + ROLES_CONFIG = { + "admin": [], + "viewer": [], + "yard": ["front_door", "backyard"], + "garage_only": ["garage"], + } + CAMERA_NAMES = {"front_door", "backyard", "garage"} + # --- IPC topic blocking (unconditional, regardless of role) --- def test_ipc_topic_blocked_for_admin(self): @@ -161,6 +171,124 @@ class TestCheckWsAuthorization(unittest.TestCase): _check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR) ) + # --- Camera-scoped PTZ access (non-admin with camera access) --- + + def test_viewer_can_ptz_camera_with_access(self): + # viewer maps to all cameras, so PTZ is allowed + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_custom_role_can_ptz_assigned_camera(self): + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_custom_role_blocked_from_ptz_unassigned_camera(self): + self.assertFalse( + _check_ws_authorization( + "garage/ptz", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_multiple_roles_union_camera_access_for_ptz(self): + # "yard" covers front_door/backyard, "garage_only" covers garage + self.assertTrue( + _check_ws_authorization( + "garage/ptz", + "yard,garage_only", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_unknown_role_blocked_from_ptz(self): + self.assertFalse( + _check_ws_authorization( + "front_door/ptz", + "nonexistent", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_no_role_header_treated_as_viewer_for_ptz(self): + # proxy-only / auth-disabled setups default to the viewer role + self.assertTrue( + _check_ws_authorization( + "front_door/ptz", + None, + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_camera_access_does_not_grant_set_commands(self): + # camera access enables PTZ only, not config-changing "set" commands + self.assertFalse( + _check_ws_authorization( + "front_door/detect/set", + "yard", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_ptz_autotracker_stays_admin_only(self): + # ptz_autotracker is a config toggle, not a live-view action + self.assertFalse( + _check_ws_authorization( + "front_door/ptz_autotracker/set", + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_admin_can_ptz_any_camera_with_config(self): + self.assertTrue( + _check_ws_authorization( + "garage/ptz", + "admin", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + + def test_ipc_topic_still_blocked_with_camera_access(self): + # IPC topics are blocked unconditionally, even with camera access + self.assertFalse( + _check_ws_authorization( + UPDATE_CAMERA_ACTIVITY, + "viewer", + self.DEFAULT_SEPARATOR, + self.ROLES_CONFIG, + self.CAMERA_NAMES, + ) + ) + if __name__ == "__main__": unittest.main() diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 84a0f390a3..6b39885c0d 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -1,7 +1,8 @@ import logging import random import string -from typing import Any, Sequence, cast +from collections.abc import Sequence +from typing import Any, cast import cv2 import numpy as np @@ -45,6 +46,17 @@ def distance(detection: np.ndarray, estimate: np.ndarray) -> float: estimate_dim = np.diff(estimate, axis=0).flatten() detection_dim = np.diff(detection, axis=0).flatten() + # Guard against degenerate or non-finite boxes + if ( + not np.all(np.isfinite(estimate_dim)) + or not np.all(np.isfinite(detection_dim)) + or estimate_dim[0] <= 0 + or estimate_dim[1] <= 0 + or detection_dim[0] <= 0 + or detection_dim[1] <= 0 + ): + return float("inf") + # get bottom center positions detection_position = np.array( [np.average(detection[:, 0]), np.max(detection[:, 1])] @@ -630,9 +642,11 @@ class NorfairTracker(ObjectTracker): self.deregister(self.track_id_map[e_id], e_id) # update list of object boxes that don't have a tracked object yet - tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()] + tracked_object_boxes = { + tuple(obj["box"]) for obj in self.tracked_objects.values() + } self.untracked_object_boxes = [ - o[2] for o in detections if o[2] not in tracked_object_boxes + o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes ] def print_objects_as_table(self, tracked_objects: Sequence) -> None: diff --git a/frigate/track/stationary_classifier.py b/frigate/track/stationary_classifier.py index bea37f641b..1e22ec6f6d 100644 --- a/frigate/track/stationary_classifier.py +++ b/frigate/track/stationary_classifier.py @@ -63,6 +63,9 @@ NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds( max_stationary_history=4, ) +# Default thresholds for any other object label +DEFAULT_OBJECT_THRESHOLDS = StationaryThresholds() + def get_stationary_threshold(label: str) -> StationaryThresholds: """Get the stationary thresholds for a given object label.""" @@ -76,7 +79,7 @@ def get_stationary_threshold(label: str) -> StationaryThresholds: if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects: return NON_STATIONARY_OBJECT_THRESHOLDS - return StationaryThresholds() + return DEFAULT_OBJECT_THRESHOLDS class StationaryMotionClassifier: diff --git a/frigate/track/tracked_object.py b/frigate/track/tracked_object.py index 03117df692..0a9c6f74f0 100644 --- a/frigate/track/tracked_object.py +++ b/frigate/track/tracked_object.py @@ -5,7 +5,7 @@ import math import os from collections import defaultdict from statistics import median -from typing import Any, Optional, cast +from typing import Any, cast import cv2 import numpy as np @@ -81,7 +81,7 @@ class TrackedObject: self.previous = self.to_dict() @property - def max_severity(self) -> Optional[str]: + def max_severity(self) -> str | None: review_config = self.camera_config.review if ( @@ -590,7 +590,7 @@ class TrackedObjectAttribute: "box": self.box, } - def find_best_object(self, objects: list[dict[str, Any]]) -> Optional[str]: + def find_best_object(self, objects: list[dict[str, Any]]) -> str | None: """Find the best attribute for each object and return its ID.""" best_object_area: float | None = None best_object_id: str | None = None diff --git a/frigate/util/audio.py b/frigate/util/audio.py index eede9c0ea0..28fb0a6bad 100644 --- a/frigate/util/audio.py +++ b/frigate/util/audio.py @@ -3,7 +3,6 @@ import logging import os import subprocess as sp -from typing import Optional from pathvalidate import sanitize_filename @@ -19,7 +18,7 @@ def get_audio_from_recording( start_ts: float, end_ts: float, sample_rate: int = 16000, -) -> Optional[bytes]: +) -> bytes | None: """Extract audio from recording files between start_ts and end_ts in WAV format suitable for sherpa-onnx. Args: diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index bd45a4a1f1..35dba2f3a7 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -2,7 +2,6 @@ import ast import copy -import datetime import logging import math import multiprocessing.queues @@ -10,17 +9,22 @@ 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 -from typing import Any, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any import numpy as np from ruamel.yaml import YAML from frigate.const import REGEX_HTTP_CAMERA_USER_PASS, REGEX_RTSP_CAMERA_USER_PASS +if TYPE_CHECKING: + from frigate.config import CameraConfig + logger = logging.getLogger(__name__) @@ -29,23 +33,20 @@ class EventsPerSecond: self._start = None self._max_events = max_events self._last_n_seconds = last_n_seconds - self._timestamps = [] + self._timestamps: deque[float] = deque(maxlen=max_events) def start(self) -> None: - self._start = datetime.datetime.now().timestamp() + self._start = time.monotonic() def update(self) -> None: - now = datetime.datetime.now().timestamp() + now = time.monotonic() 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 = datetime.datetime.now().timestamp() + now = time.monotonic() if self._start is None: self._start = now # compute the (approximate) events in the last n seconds @@ -60,7 +61,7 @@ class EventsPerSecond: def expire_timestamps(self, now: float) -> None: threshold = now - self._last_n_seconds while self._timestamps and self._timestamps[0] < threshold: - del self._timestamps[0] + self._timestamps.popleft() class InferenceSpeed: @@ -132,8 +133,26 @@ def get_ffmpeg_arg_list(arg: Any) -> list: return arg if isinstance(arg, list) else shlex.split(arg) +# all built-in record presets use this segment_time +DEFAULT_RECORD_SEGMENT_TIME = 10 + + +def get_record_segment_time(config: "CameraConfig") -> int: + """Extract -segment_time from the camera's record output args.""" + record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record) + + if record_args and record_args[0].startswith("preset"): + return DEFAULT_RECORD_SEGMENT_TIME + + try: + idx = record_args.index("-segment_time") + return int(record_args[idx + 1]) + except (ValueError, IndexError): + return DEFAULT_RECORD_SEGMENT_TIME + + def load_labels( - path: Optional[str], encoding="utf-8", prefill=91, indexed: bool | None = None + path: str | None, encoding="utf-8", prefill=91, indexed: bool | None = None ): """Loads labels from file (with or without index numbers). Args: @@ -145,7 +164,7 @@ def load_labels( if path is None: return {} - with open(path, "r", encoding=encoding) as f: + with open(path, encoding=encoding) as f: labels = {index: "unknown" for index in range(prefill)} lines = f.readlines() if not lines: @@ -161,8 +180,8 @@ def load_labels( def to_relative_box( - width: int, height: int, box: Tuple[int, int, int, int] -) -> Tuple[int | float, int | float, int | float, int | float]: + width: int, height: int, box: tuple[int, int, int, int] +) -> tuple[int | float, int | float, int | float, int | float]: return ( box[0] / width, # x box[1] / height, # y @@ -176,7 +195,7 @@ def create_mask(frame_shape, mask): mask_img[:] = 255 -def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any]: +def process_config_query_string(query_string: dict[str, list]) -> dict[str, Any]: updates = {} for key_path_str, new_value_list in query_string.items(): # use the string key as-is for updates dictionary @@ -194,8 +213,8 @@ def process_config_query_string(query_string: Dict[str, list]) -> Dict[str, Any] def flatten_config_data( - config_data: Dict[str, Any], parent_key: str = "" -) -> Dict[str, Any]: + config_data: dict[str, Any], parent_key: str = "" +) -> dict[str, Any]: items = [] for key, value in config_data.items(): escaped_key = escape_config_key_segment(str(key)) @@ -242,12 +261,12 @@ def split_config_key_path(key_path_str: str) -> list[str]: return parts -def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]): +def update_yaml_file_bulk(file_path: str, updates: dict[str, Any]): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) try: - with open(file_path, "r") as f: + with open(file_path) as f: data = yaml.load(f) except FileNotFoundError: logger.error( @@ -274,26 +293,51 @@ def update_yaml_file_bulk(file_path: str, updates: Dict[str, Any]): logger.error(f"Unable to write to Frigate config file {file_path}: {e}") +def clear_orphaned_comments(collection, parent, parent_key) -> None: + """Drop stale ruamel comment tokens after a deletion empties a collection. + + When the last entry of a mapping or sequence is removed, any comments that + lived inside that collection's block are orphaned. ruamel then emits them + above a flow-style `{}`/`[]` dedented to column 0, which is unparseable and + corrupts the config. Clearing the emptied collection's own comment metadata + (and the parent's entry pointing at it) keeps the dump valid. Non-empty + collections are left untouched so comments on remaining siblings survive. + """ + if not hasattr(collection, "ca") or len(collection) != 0: + return + + collection.ca.items.clear() + collection.ca.comment = None + if parent is not None and hasattr(parent, "ca"): + parent.ca.items.pop(parent_key, None) + + def update_yaml(data, key_path, new_value): temp = data + parent = None + parent_key = None for key in key_path[:-1]: if isinstance(key, tuple): if key[0] not in temp: temp[key[0]] = [{}] * max(1, key[1] + 1) elif len(temp[key[0]]) <= key[1]: temp[key[0]] += [{}] * (key[1] - len(temp[key[0]]) + 1) + parent, parent_key = temp[key[0]], key[1] temp = temp[key[0]][key[1]] else: if key not in temp or temp[key] is None: temp[key] = {} + parent, parent_key = temp, key temp = temp[key] last_key = key_path[-1] if new_value == "": if isinstance(last_key, tuple): del temp[last_key[0]][last_key[1]] + clear_orphaned_comments(temp[last_key[0]], temp, last_key[0]) else: del temp[last_key] + clear_orphaned_comments(temp, parent, parent_key) else: if isinstance(last_key, tuple): if last_key[0] not in temp: @@ -394,9 +438,7 @@ def generate_color_palette(n): return colors -def serialize( - vector: Union[list[float], np.ndarray, float], pack: bool = True -) -> bytes: +def serialize(vector: list[float] | np.ndarray | float, pack: bool = True) -> bytes: """Serializes a list of floats, numpy array, or single float into a compact "raw bytes" format""" if isinstance(vector, np.ndarray): # Convert numpy array to list of floats @@ -415,7 +457,7 @@ def serialize( else: return vector except struct.error as e: - raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") + raise ValueError(f"Failed to pack vector: {e}. Vector: {vector}") from e def deserialize(bytes_data: bytes) -> list[float]: diff --git a/frigate/util/classification.py b/frigate/util/classification.py index 30fc4c6687..a9345bbc56 100644 --- a/frigate/util/classification.py +++ b/frigate/util/classification.py @@ -84,7 +84,7 @@ def read_training_metadata(model_name: str) -> dict[str, any] | None: return None try: - with open(metadata_path, "r") as f: + with open(metadata_path) as f: metadata = json.load(f) return metadata except Exception as e: @@ -294,7 +294,7 @@ class ClassificationTrainingProcess(FrigateProcess): return True except Exception as e: - logger.error(f"Training failed for {self.model_name}: {e}", exc_info=True) + logger.exception(f"Training failed for {self.model_name}: {e}") return False @@ -732,7 +732,7 @@ def collect_object_classification_examples( # Step 1: Query events for the specified label and cameras events = list( - Event.select().where((Event.label == label)).order_by(Event.start_time.asc()) + Event.select().where(Event.label == label).order_by(Event.start_time.asc()) ) if not events: diff --git a/frigate/util/config.py b/frigate/util/config.py index 5a4f3816c0..3e093a9783 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -4,7 +4,7 @@ import asyncio import logging import os import shutil -from typing import Any, Optional, Union +from typing import Any from ruamel.yaml import YAML @@ -78,7 +78,7 @@ def migrate_frigate_config(config_file: str): yaml = YAML() yaml.indent(mapping=2, sequence=4, offset=2) - with open(config_file, "r") as f: + with open(config_file) as f: config: dict[str, dict[str, Any]] = yaml.load(f) if config is None: @@ -477,7 +477,7 @@ def migrate_017_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] def _convert_legacy_mask_to_dict( - mask: Optional[Union[str, list]], mask_type: str = "motion_mask", label: str = "" + mask: str | list | None, mask_type: str = "motion_mask", label: str = "" ) -> dict[str, dict[str, Any]]: """Convert legacy mask format (str or list[str]) to new dict format. @@ -659,10 +659,10 @@ def migrate_018_0(config: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any] def get_relative_coordinates( - mask: Optional[Union[str, list]], + mask: str | list | None, frame_shape: tuple[int, int], camera_name: str = "", -) -> Union[str, list]: +) -> str | list: # masks and zones are saved as relative coordinates # we know if any points are > 1 then it is using the # old native resolution coordinates @@ -720,7 +720,7 @@ def get_relative_coordinates( def convert_area_to_pixels( - area_value: Union[int, float], frame_shape: tuple[int, int] + area_value: int | float, frame_shape: tuple[int, int] ) -> int: """ Convert area specification to pixels. @@ -762,7 +762,7 @@ class StreamInfoRetriever: return info -def apply_section_update(camera_config, section: str, update: dict) -> Optional[str]: +def apply_section_update(camera_config, section: str, update: dict) -> str | None: """Merge an update dict into a camera config section and rebuild runtime variants. For motion and object filter sections, the plain Pydantic models are rebuilt diff --git a/frigate/util/downloader.py b/frigate/util/downloader.py index ee80b38165..a8b593f159 100644 --- a/frigate/util/downloader.py +++ b/frigate/util/downloader.py @@ -1,8 +1,8 @@ import logging import os import threading +from collections.abc import Callable from pathlib import Path -from typing import Callable, List import requests @@ -19,7 +19,7 @@ class ModelDownloader: self, model_name: str, download_path: str, - file_names: List[str], + file_names: list[str], download_func: Callable[[str], None], complete_func: Callable[[], None] | None = None, silent: bool = False, diff --git a/frigate/util/ffmpeg.py b/frigate/util/ffmpeg.py index 9f4c5569ab..87601b91d6 100644 --- a/frigate/util/ffmpeg.py +++ b/frigate/util/ffmpeg.py @@ -2,7 +2,8 @@ import logging import subprocess as sp -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any from frigate.const import PROCESS_PRIORITY_LOW from frigate.log import LogPipe @@ -68,9 +69,9 @@ def run_ffmpeg_with_progress( cmd: list[str], *, expected_duration_seconds: float, - on_progress: Optional[Callable[[float], None]] = None, - stdin_payload: Optional[str] = None, - process_started: Optional[Callable[[sp.Popen], None]] = None, + on_progress: Callable[[float], None] | None = None, + stdin_payload: str | None = None, + process_started: Callable[[sp.Popen], None] | None = None, use_low_priority: bool = True, ) -> tuple[int, str]: """Run an ffmpeg command, streaming progress via `-progress pipe:2`. diff --git a/frigate/util/file.py b/frigate/util/file.py index f858dca68e..e259d13456 100644 --- a/frigate/util/file.py +++ b/frigate/util/file.py @@ -7,7 +7,7 @@ import os import time from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any import cv2 from numpy import ndarray @@ -323,7 +323,7 @@ class FileLock: self.timeout = timeout self.poll_interval = poll_interval self.stale_timeout = stale_timeout - self._fd: Optional[int] = None + self._fd: int | None = None self._acquired = False if cleanup_stale_on_init: @@ -367,7 +367,7 @@ class FileLock: return False - def acquire(self, timeout: Optional[int] = None) -> bool: + def acquire(self, timeout: int | None = None) -> bool: """ Acquire the file lock using fcntl.flock(). @@ -400,7 +400,7 @@ class FileLock: self._acquired = True logger.debug(f"Acquired lock: {self.lock_path}") return True - except (OSError, IOError): + except OSError: # Lock is held by another process if time.time() - start_time >= timeout: logger.warning(f"Timeout waiting for lock: {self.lock_path}") diff --git a/frigate/util/image.py b/frigate/util/image.py index d2832d97a0..dde25e2425 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from multiprocessing import resource_tracker as _mprt from multiprocessing import shared_memory as _mpshm from string import printable -from typing import Any, AnyStr, Optional +from typing import Any, AnyStr import cv2 import numpy as np @@ -270,7 +270,7 @@ def draw_box_with_label( ) -def get_image_quality_params(ext: str, quality: Optional[int]) -> list[int]: +def get_image_quality_params(ext: str, quality: int | None) -> list[int]: if ext in ("jpg", "jpeg"): return [int(cv2.IMWRITE_JPEG_QUALITY), quality if quality is not None else 70] @@ -921,7 +921,7 @@ def yuv_region_2_bgr(frame, region): raise -def intersection(box_a, box_b) -> Optional[list[int]]: +def intersection(box_a, box_b) -> list[int] | None: """Return intersection box or None if boxes do not intersect.""" if ( box_a[2] < box_b[0] @@ -994,7 +994,7 @@ class FrameManager(ABC): pass @abstractmethod - def write(self, name: str) -> Optional[memoryview]: + def write(self, name: str) -> memoryview | None: pass @abstractmethod @@ -1021,7 +1021,7 @@ class UntrackedSharedMemory(_mpshm.SharedMemory): def __init__( self, - name: Optional[str] = None, + name: str | None = None, create: bool = False, size: int = 0, *, @@ -1075,7 +1075,7 @@ class SharedMemoryFrameManager(FrameManager): self.shm_store[name] = shm return shm.buf - def write(self, name: str) -> Optional[memoryview]: + def write(self, name: str) -> memoryview | None: try: if name in self.shm_store: shm = self.shm_store[name] @@ -1087,7 +1087,7 @@ class SharedMemoryFrameManager(FrameManager): logger.info(f"the file {name} not found") return None - def get(self, name: str, shape) -> Optional[np.ndarray]: + def get(self, name: str, shape) -> np.ndarray | None: try: required = int(np.prod(shape)) shm = self.shm_store.get(name) @@ -1185,10 +1185,10 @@ def run_ffmpeg_snapshot( ffmpeg, input_path: str, codec: str, - seek_time: Optional[float] = None, - height: Optional[int] = None, - timeout: Optional[int] = None, -) -> tuple[Optional[bytes], str]: + seek_time: float | None = None, + height: int | None = None, + timeout: int | None = None, +) -> tuple[bytes | None, str]: """Run ffmpeg to extract a snapshot/image from a video source.""" ffmpeg_cmd = [ ffmpeg.ffmpeg_path, @@ -1238,8 +1238,8 @@ def get_image_from_recording( file_path: str, relative_frame_time: float, codec: str, - height: Optional[int] = None, -) -> Optional[Any]: + height: int | None = None, +) -> Any | None: """retrieve a frame from given time in recording file.""" image_data, _ = run_ffmpeg_snapshot( @@ -1261,7 +1261,7 @@ def get_histogram(image, x_min, y_min, x_max, y_max): def create_thumbnail( yuv_frame: np.ndarray, box: tuple[int, int, int, int], height=500 -) -> Optional[bytes]: +) -> bytes | None: """Return jpg thumbnail of a region of the frame.""" frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2BGR_I420) region = calculate_region( diff --git a/frigate/util/media.py b/frigate/util/media.py index 31374c5960..e4e84e9149 100644 --- a/frigate/util/media.py +++ b/frigate/util/media.py @@ -5,9 +5,9 @@ import errno import logging import os import subprocess as sp +from collections.abc import Iterable from dataclasses import dataclass, field from pathlib import Path -from typing import Iterable from peewee import DatabaseError, chunked @@ -110,7 +110,7 @@ def sync_recordings( # start checking on the hour 36 hours ago check_point = datetime.datetime.now().replace( minute=0, second=0, microsecond=0 - ).astimezone(datetime.timezone.utc) - datetime.timedelta(hours=36) + ).astimezone(datetime.UTC) - datetime.timedelta(hours=36) # Gather DB recordings to inspect if limited: @@ -793,7 +793,7 @@ def write_orphan_report( f.write("# Media Sync Orphan Report\n") f.write(f"# Job: {job_id}\n") f.write( - f"# Date: {datetime.datetime.now().astimezone(datetime.timezone.utc).isoformat()}\n" + f"# Date: {datetime.datetime.now().astimezone(datetime.UTC).isoformat()}\n" ) f.write(f"# Mode: dry_run={dry_run}\n\n") diff --git a/frigate/util/object.py b/frigate/util/object.py index 7c7edc10c5..b4ee72dc07 100644 --- a/frigate/util/object.py +++ b/frigate/util/object.py @@ -339,18 +339,13 @@ def reduce_boxes(boxes, iou_threshold=0.0): def average_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: """Return a box that is the average of a list of boxes.""" - x_mins = [] - y_mins = [] - x_max = [] - y_max = [] - - for box in boxes: - x_mins.append(box[0]) - y_mins.append(box[1]) - x_max.append(box[2]) - y_max.append(box[3]) - - return [np.mean(x_mins), np.mean(y_mins), np.mean(x_max), np.mean(y_max)] + n = len(boxes) + return [ + sum(box[0] for box in boxes) / n, + sum(box[1] for box in boxes) / n, + sum(box[2] for box in boxes) / n, + sum(box[3] for box in boxes) / n, + ] def median_of_boxes(boxes: list[list[int, int, int, int]]) -> list[int, int, int, int]: @@ -401,13 +396,13 @@ def get_cluster_candidates(frame_shape, min_region, boxes): # determined by the max_region size minus half the box + 20% # TODO: see if we can do this with numpy cluster_candidates = [] - used_boxes = [] + used_boxes = set() # loop over each box for current_index, b in enumerate(boxes): if current_index in used_boxes: continue cluster = [current_index] - used_boxes.append(current_index) + used_boxes.add(current_index) cluster_boundary = get_cluster_boundary(b, min_region) # find all other boxes that fit inside the boundary for compare_index, compare_box in enumerate(boxes): @@ -436,7 +431,7 @@ def get_cluster_candidates(frame_shape, min_region, boxes): if should_cluster: cluster.append(compare_index) - used_boxes.append(compare_index) + used_boxes.add(compare_index) cluster_candidates.append(cluster) # return the unique clusters only @@ -558,6 +553,7 @@ def reduce_detections( current_detection = sorted_by_area[current_detection_idx] current_label = current_detection[0] current_box = current_detection[2] + current_area = area(current_box) overlap = 0 for to_check_idx in range( min(current_detection_idx + 1, len(sorted_by_area)), @@ -568,14 +564,14 @@ def reduce_detections( # if area of current detection / area of check < 5% they should not be compared # this covers cases where a large car parked in a driveway doesn't block detections # of cars in the street behind it - if area(current_box) / area(to_check) < 0.05: + if current_area / area(to_check) < 0.05: continue intersect_box = intersection(current_box, to_check) # if % of smaller detection is inside of another detection, consolidate - if intersect_box is not None and area(intersect_box) / area( - current_box - ) > LABEL_CONSOLIDATION_MAP.get( + if intersect_box is not None and area( + intersect_box + ) / current_area > LABEL_CONSOLIDATION_MAP.get( current_label, LABEL_CONSOLIDATION_DEFAULT ): overlap = 1 diff --git a/frigate/util/process.py b/frigate/util/process.py index 1613c1e431..060c39c7b1 100644 --- a/frigate/util/process.py +++ b/frigate/util/process.py @@ -6,9 +6,9 @@ import os import pathlib import subprocess import threading +from collections.abc import Callable from logging.handlers import QueueHandler from multiprocessing.synchronize import Event as MpEvent -from typing import Callable, Optional from setproctitle import setproctitle @@ -23,11 +23,11 @@ class BaseProcess(mp.Process): stop_event: MpEvent, priority: int, *, - name: Optional[str] = None, - target: Optional[Callable] = None, + name: str | None = None, + target: Callable | None = None, args: tuple = (), kwargs: dict = {}, - daemon: Optional[bool] = None, + daemon: bool | None = None, ): self.priority = priority self.stop_event = stop_event @@ -121,7 +121,7 @@ class FrigateProcess(BaseProcess): f"If process crashes, manually generate with: memray flamegraph {binary_file}" ) except Exception as e: - self.logger.error(f"Failed to setup memray profiling: {e}", exc_info=True) + self.logger.exception(f"Failed to setup memray profiling: {e}") def _cleanup_memray(self, safe_name: str, binary_file: pathlib.Path) -> None: """Stop memray tracking and generate HTML report.""" @@ -156,4 +156,4 @@ class FrigateProcess(BaseProcess): except subprocess.TimeoutExpired: self.logger.error("Memray report generation timed out") except Exception as e: - self.logger.error(f"Failed to cleanup memray profiling: {e}", exc_info=True) + self.logger.exception(f"Failed to cleanup memray profiling: {e}") diff --git a/frigate/util/rknn_converter.py b/frigate/util/rknn_converter.py index 5660c7601d..ad387413d4 100644 --- a/frigate/util/rknn_converter.py +++ b/frigate/util/rknn_converter.py @@ -6,7 +6,6 @@ import subprocess import sys import time from pathlib import Path -from typing import Optional from frigate.const import SUPPORTED_RK_SOCS from frigate.util.file import FileLock @@ -139,7 +138,7 @@ def ensure_rknn_toolkit() -> bool: return False -def get_soc_type() -> Optional[str]: +def get_soc_type() -> str | None: """Get the SoC type from device tree.""" try: with open("/proc/device-tree/compatible") as file: @@ -160,7 +159,7 @@ def convert_onnx_to_rknn( output_path: str, model_type: str, quantization: bool = False, - soc: Optional[str] = None, + soc: str | None = None, ) -> bool: """ Convert ONNX model to RKNN format. @@ -345,7 +344,7 @@ def wait_for_conversion_completion( def auto_convert_model( model_path: str, model_type: str | None = None, quantization: bool = False -) -> Optional[str]: +) -> str | None: """ Automatically convert a model to RKNN format if needed. diff --git a/frigate/util/schema.py b/frigate/util/schema.py index 5ba1bc0619..9af651ea07 100644 --- a/frigate/util/schema.py +++ b/frigate/util/schema.py @@ -1,11 +1,11 @@ """JSON schema utilities for Frigate.""" -from typing import Any, Dict, Type +from typing import Any from pydantic import BaseModel, TypeAdapter -def get_config_schema(config_class: Type[BaseModel]) -> Dict[str, Any]: +def get_config_schema(config_class: type[BaseModel]) -> dict[str, Any]: """ Returns the JSON schema for FrigateConfig with polymorphic detectors. diff --git a/frigate/util/services.py b/frigate/util/services.py index 0445053b8a..38737907ef 100644 --- a/frigate/util/services.py +++ b/frigate/util/services.py @@ -12,7 +12,7 @@ import subprocess as sp import time import traceback from datetime import datetime -from typing import Any, List, Optional, Tuple +from typing import Any import cv2 import psutil @@ -59,7 +59,7 @@ def get_cgroups_version() -> str: return "unknown" try: - with open("/proc/mounts", "r") as f: + with open("/proc/mounts") as f: mounts = f.readlines() for mount in mounts: @@ -89,7 +89,7 @@ def get_docker_memlimit_bytes() -> int: memlimit_path = "/sys/fs/cgroup/memory.max" try: - with open(memlimit_path, "r") as f: + with open(memlimit_path) as f: value = f.read().strip() if value.isnumeric(): @@ -127,7 +127,7 @@ def get_cpu_stats() -> dict[str, dict]: if not any(keyword in cmdline for keyword in keywords): continue - with open(f"/proc/{pid}/stat", "r") as f: + with open(f"/proc/{pid}/stat") as f: stats = f.readline().split() utime = int(stats[13]) stime = int(stats[14]) @@ -146,7 +146,7 @@ def get_cpu_stats() -> dict[str, dict]: process_usage_sec = process_utime_sec + process_stime_sec cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec - with open(f"/proc/{pid}/statm", "r") as f: + with open(f"/proc/{pid}/statm") as f: mem_stats = f.readline().split() mem_res = int(mem_stats[1]) * os.sysconf("SC_PAGE_SIZE") / 1024 @@ -171,7 +171,7 @@ def get_physical_interfaces(interfaces) -> list: if not interfaces: return [] - with open("/proc/net/dev", "r") as file: + with open("/proc/net/dev") as file: lines = file.readlines() physical_interfaces = [] @@ -238,7 +238,7 @@ def is_vaapi_amd_driver() -> bool: return any("AMD Radeon Graphics" in line for line in output) -def get_amd_gpu_stats() -> Optional[dict[str, str]]: +def get_amd_gpu_stats() -> dict[str, str] | None: """Get stats using radeontop.""" radeontop_command = ["radeontop", "-d", "-", "-l", "1"] @@ -285,38 +285,83 @@ _XE_ENGINE_KEYS = { "vecs": "video-enhance", "ccs": "compute", } +_INTEL_DRM_DRIVERS = ("i915", "xe") +_PCI_ADDRESS_RE = re.compile( + r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$" +) -def _resolve_intel_gpu_pdev(device: Optional[str]) -> Optional[str]: +def _resolve_intel_gpu_pdev(device: str | None) -> str | None: """Map a configured GPU hint (/dev/dri/card1, renderD128, or a PCI bus address) to its drm-pdev string so we can filter fdinfo entries to that device. Returns None when no hint is supplied or it cannot be resolved.""" if not device: return None - if re.match(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$", device): + if _PCI_ADDRESS_RE.match(device): return device name = os.path.basename(device.rstrip("/")) try: - return os.path.basename(os.path.realpath(f"/sys/class/drm/{name}/device")) + pdev = os.path.basename(os.path.realpath(f"/sys/class/drm/{name}/device")) except OSError: return None + # realpath does not raise on a nonexistent node; it returns the input + # path unchanged, so validate the result actually looks like a PCI + # address before trusting it. + return pdev if _PCI_ADDRESS_RE.match(pdev) else None -def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict: + +def _enumerate_drm_devices() -> dict[str, str]: + """Map each PCI-attached DRM device to its bound kernel driver. + + Reads /sys/class/drm, which reflects every GPU on the host even when only + some render nodes are mapped into the container, so device presence can be + verified without /dev access. Returns {pdev: driver}, e.g. + {"0000:00:02.0": "i915"}. + """ + devices: dict[str, str] = {} + + try: + entries = os.listdir("/sys/class/drm") + except OSError: + return devices + + for entry in entries: + device_dir = f"/sys/class/drm/{entry}/device" + pdev = os.path.basename(os.path.realpath(device_dir)) + + if not _PCI_ADDRESS_RE.match(pdev): + continue + + try: + driver = os.path.basename(os.readlink(f"{device_dir}/driver")) + except OSError: + continue + + devices[pdev] = driver + + return devices + + +def _read_intel_drm_fdinfo(target_pdev: str | None) -> dict | None: """Snapshot DRM fdinfo for every Intel client visible in /proc. Returns a dict keyed by (pdev, drm-client-id, pid) so the same context seen via multiple file descriptors on a single process collapses to one - entry. + entry. Clients whose fdinfo carries no engine counters are still included + with an empty "engines" dict so the caller can distinguish "clients exist + but the kernel publishes no busyness" from "no clients at all". Returns + None when /proc itself cannot be scanned, which is a different failure + than a scan that finds nothing. """ snapshot: dict = {} try: proc_entries = os.listdir("/proc") except OSError: - return snapshot + return None for entry in proc_entries: if not entry.isdigit(): @@ -360,7 +405,7 @@ def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict: if key in snapshot: continue - engines: dict[str, tuple[int, int]] = {} + engines: dict[str, tuple[int, int, int]] = {} if driver == "i915": for fkey, engine in _I915_ENGINE_KEYS.items(): @@ -368,58 +413,156 @@ def _read_intel_drm_fdinfo(target_pdev: Optional[str]) -> dict: if not raw: continue try: - engines[engine] = (int(raw.split()[0]), 0) + engines[engine] = (int(raw.split()[0]), 0, 1) except (ValueError, IndexError): continue else: for suffix, engine in _XE_ENGINE_KEYS.items(): busy_raw = fields.get(f"drm-cycles-{suffix}") total_raw = fields.get(f"drm-total-cycles-{suffix}") + if not (busy_raw and total_raw): continue + + # drm-cycles-* is summed across every instance of the engine + # class while drm-total-cycles-* tracks a single instance, so + # busy/total scales up to the capacity (e.g. Battlemage + # reports 2 for vcs/vecs). Capture it to divide back out; + # absent means a single engine, so default to 1. + capacity_raw = fields.get(f"drm-engine-capacity-{suffix}") + + try: + capacity = int(capacity_raw.split()[0]) if capacity_raw else 1 + except (ValueError, IndexError): + capacity = 1 + try: engines[engine] = ( int(busy_raw.split()[0]), int(total_raw.split()[0]), + max(1, capacity), ) except (ValueError, IndexError): continue - if not engines: - continue - snapshot[key] = {"driver": driver, "pid": entry, "engines": engines} return snapshot +def _idle_intel_gpu_stats( + target_pdev: str | None, intel_pdevs: dict[str, str] +) -> dict[str, dict[str, Any]]: + """Build a 0% reading for the configured (or every) Intel GPU. + + Used when the device is confirmed present but no DRM client is currently + attached, e.g. while camera processes are restarting. That is an idle + state, not a collection failure, so it must produce a valid reading: + returning None would latch the hwaccel error cooldown and blank GPU stats + for an hour over a momentary gap. + """ + from frigate.stats.intel_gpu_info import intel_gpu_name_resolver + + names = intel_gpu_name_resolver.get_names() + pdevs = [target_pdev] if target_pdev else sorted(intel_pdevs) + + return { + pdev: { + "name": names.get(pdev) or "Intel iGPU", + "vendor": "intel", + "gpu": "0.0%", + "mem": "-%", + "compute": "0.0%", + "dec": "0.0%", + } + for pdev in pdevs + } + + def get_intel_gpu_stats( - intel_gpu_device: Optional[str], -) -> Optional[dict[str, dict[str, Any]]]: + intel_gpu_device: str | None, +) -> dict[str, dict[str, Any]] | None: """Get stats by reading DRM fdinfo files, bucketed per-pdev. Each DRM client FD exposes monotonic per-engine busy counters via - /proc//fdinfo/ (i915 since kernel 5.19, Xe since first release). - We sample twice and divide busy-time deltas by wall-clock to derive - utilization. Render/3D and Compute are pooled into "compute"; Video and - VideoEnhance into "dec". Overall "gpu" is the sum of those pools (clamped - to 100%). + /proc//fdinfo/. For i915 this requires kernel 6.5 or newer: + earlier kernels omit the per-engine counters whenever GuC submission is + active, which is the default on 12th gen and newer. Xe has exposed them + since its first release. We sample twice and divide busy-time deltas by + wall-clock to derive utilization. Render/3D and Compute are pooled into + "compute"; Video and VideoEnhance into "dec". Overall "gpu" is the sum of + those pools (clamped to 100%). The return value is keyed by the GPU's drm-pdev string so multiple Intel GPUs in the same system are reported separately. Each entry carries a "name" populated from OpenVINO (falling back to the pdev) so callers can surface a real device name in the UI. + + A device that exists but has no attached DRM clients reports an idle 0% + reading. None is returned only for durable failures (no Intel GPU, a bad + intel_gpu_device config, unreadable /proc, or a kernel that publishes no + counters), each of which logs a distinct warning, and the caller latches + it against retries for an hour. """ from frigate.stats.intel_gpu_info import intel_gpu_name_resolver target_pdev = _resolve_intel_gpu_pdev(intel_gpu_device) + if intel_gpu_device and not target_pdev: + logger.warning( + "Unable to collect Intel GPU stats: configured intel_gpu_device %s " + "does not exist or could not be resolved to a PCI device", + intel_gpu_device, + ) + return None + + drm_devices = _enumerate_drm_devices() + intel_pdevs = { + pdev: driver + for pdev, driver in drm_devices.items() + if driver in _INTEL_DRM_DRIVERS + } + + if not intel_pdevs: + logger.warning( + "Unable to collect Intel GPU stats: no Intel GPU (i915/xe) found in " + "/sys/class/drm. Check that the driver is loaded on the host" + ) + return None + + if target_pdev and target_pdev not in intel_pdevs: + logger.warning( + "Unable to collect Intel GPU stats: configured intel_gpu_device %s " + "resolved to %s (driver: %s), which is not an Intel GPU", + intel_gpu_device, + target_pdev, + drm_devices.get(target_pdev, "unknown"), + ) + return None snapshot_a = _read_intel_drm_fdinfo(target_pdev) + if snapshot_a is None: + logger.warning("Unable to collect Intel GPU stats: /proc could not be read") + return None + if not snapshot_a: + # No process currently holds the GPU open, e.g. while camera processes + # are restarting. The device is confirmed present, so report idle + # rather than an error; the next stats cycle re-samples normally. + logger.debug("No active DRM clients for Intel GPU, reporting idle") + return _idle_intel_gpu_stats(target_pdev, intel_pdevs) + + if not any(client["engines"] for client in snapshot_a.values()): + # Clients exist but the kernel published no busyness for them, so + # there is nothing to sample and a second snapshot would not help. + # i915 suppresses per-client engine counters while GuC submission is + # active on kernels older than 6.5 (kernel commit 1324680a80eb lifted + # this), which covers stock Debian 12 and Ubuntu 22.04 on 12th gen + # and newer. logger.warning( - "Unable to collect Intel GPU stats: no DRM fdinfo entries found" - "%s. Check that /proc is readable and the i915/xe driver is loaded", - f" for pdev {target_pdev}" if target_pdev else "", + "Unable to collect Intel GPU stats: found %d DRM client(s) for %s but " + "no per-engine counters. Kernel 6.5 or newer is required.", + len(snapshot_a), + "/".join(sorted({client["driver"] for client in snapshot_a.values()})), ) return None @@ -428,12 +571,18 @@ def get_intel_gpu_stats( elapsed_ns = (time.monotonic() - start) * 1e9 snapshot_b = _read_intel_drm_fdinfo(target_pdev) - if not snapshot_b or elapsed_ns <= 0: - logger.warning( - "Unable to collect Intel GPU stats: second DRM fdinfo sample was empty" - ) + if snapshot_b is None: + logger.warning("Unable to collect Intel GPU stats: /proc could not be read") return None + if not snapshot_b or elapsed_ns <= 0: + # Every client disappeared during the sample window; transient by + # definition, so report idle instead of latching an error. + logger.debug( + "No DRM clients persisted across Intel GPU samples, reporting idle" + ) + return _idle_intel_gpu_stats(target_pdev, intel_pdevs) + def _new_engine_pct() -> dict[str, float]: return {"render": 0.0, "video": 0.0, "video-enhance": 0.0, "compute": 0.0} @@ -445,16 +594,23 @@ def get_intel_gpu_stats( if not data_a or data_a["driver"] != data_b["driver"]: continue + # Skip before the setdefault below so a counter-less client cannot + # register its pdev on its own. + if not data_b["engines"]: + continue + pdev = key[0] engine_pct = per_pdev_engine_pct.setdefault(pdev, _new_engine_pct()) pid_pct = per_pdev_pid_pct.setdefault(pdev, {}) client_total = 0.0 - for engine, (busy_b, total_b) in data_b["engines"].items(): + for engine, (busy_b, total_b, capacity) in data_b["engines"].items(): if engine not in engine_pct: continue - busy_a, total_a = data_a["engines"].get(engine, (busy_b, total_b)) + busy_a, total_a, _ = data_a["engines"].get( + engine, (busy_b, total_b, capacity) + ) if data_b["driver"] == "i915": delta = max(0, busy_b - busy_a) @@ -464,7 +620,9 @@ def get_intel_gpu_stats( delta_total = total_b - total_a if delta_total <= 0: continue - pct = min(100.0, delta_busy / delta_total * 100.0) + # Normalize by capacity so a class with N engine instances + # (busy summed across all N) reports 0-100%, not 0-N*100%. + pct = min(100.0, delta_busy / (delta_total * capacity) * 100.0) engine_pct[engine] += pct client_total += pct @@ -472,11 +630,12 @@ def get_intel_gpu_stats( pid_pct[data_b["pid"]] = pid_pct.get(data_b["pid"], 0.0) + client_total if not per_pdev_engine_pct: - logger.warning( - "Unable to collect Intel GPU stats: no per-engine counters available " - "(i915 requires kernel >= 5.19)" + # Clients were seen in both snapshots but none persisted as the same + # (pdev, client-id, pid); process churn, so report idle. + logger.debug( + "No DRM clients persisted across Intel GPU samples, reporting idle" ) - return None + return _idle_intel_gpu_stats(target_pdev, intel_pdevs) names = intel_gpu_name_resolver.get_names() results: dict[str, dict[str, Any]] = {} @@ -509,12 +668,12 @@ def get_intel_gpu_stats( return results -def get_openvino_npu_stats() -> Optional[dict[str, str]]: +def get_openvino_npu_stats() -> dict[str, str] | None: """Get NPU stats using openvino.""" NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time" try: - with open(NPU_RUNTIME_PATH, "r") as f: + with open(NPU_RUNTIME_PATH) as f: initial_runtime = float(f.read().strip()) initial_time = time.time() @@ -523,7 +682,7 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]: time.sleep(1.0) # Read runtime value again - with open(NPU_RUNTIME_PATH, "r") as f: + with open(NPU_RUNTIME_PATH) as f: current_runtime = float(f.read().strip()) current_time = time.time() @@ -542,10 +701,10 @@ def get_openvino_npu_stats() -> Optional[dict[str, str]]: return None -def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: +def get_rockchip_gpu_stats() -> dict[str, str | float] | None: """Get GPU stats using rk.""" try: - with open("/sys/kernel/debug/rkrga/load", "r") as f: + with open("/sys/kernel/debug/rkrga/load") as f: content = f.read() except FileNotFoundError: return None @@ -563,7 +722,7 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: stats: dict[str, str | float] = {"gpu": average_load, "mem": "-%"} try: - with open("/sys/class/thermal/thermal_zone5/temp", "r") as f: + with open("/sys/class/thermal/thermal_zone5/temp") as f: line = f.readline().strip() stats["temp"] = round(int(line) / 1000, 1) except (FileNotFoundError, OSError, ValueError): @@ -572,10 +731,10 @@ def get_rockchip_gpu_stats() -> Optional[dict[str, str | float]]: return stats -def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: +def get_rockchip_npu_stats() -> dict[str, float | str] | None: """Get NPU stats using rk.""" try: - with open("/sys/kernel/debug/rknpu/load", "r") as f: + with open("/sys/kernel/debug/rknpu/load") as f: npu_output = f.read() if "Core0:" in npu_output: @@ -595,7 +754,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: stats: dict[str, float | str] = {"npu": mean, "mem": "-%"} try: - with open("/sys/class/thermal/thermal_zone6/temp", "r") as f: + with open("/sys/class/thermal/thermal_zone6/temp") as f: line = f.readline().strip() stats["temp"] = round(int(line) / 1000, 1) except (FileNotFoundError, OSError, ValueError): @@ -604,7 +763,7 @@ def get_rockchip_npu_stats() -> Optional[dict[str, float | str]]: return stats -def get_axcl_npu_stats() -> Optional[dict[str, str | float]]: +def get_axcl_npu_stats() -> dict[str, str | float] | None: """Get NPU stats using axcl.""" # Check if axcl-smi exists axcl_smi_path = "/usr/bin/axcl/axcl-smi" @@ -721,18 +880,18 @@ def get_nvidia_gpu_stats() -> dict[int, dict]: return results -def get_jetson_stats() -> Optional[dict[int, dict]]: +def get_jetson_stats() -> dict[int, dict] | None: results = {} try: results["mem"] = "-" # no discrete gpu memory if os.path.exists("/sys/devices/gpu.0/load"): - with open("/sys/devices/gpu.0/load", "r") as f: + with open("/sys/devices/gpu.0/load") as f: gpuload = float(f.readline()) / 10 results["gpu"] = f"{gpuload}%" elif os.path.exists("/sys/devices/platform/gpu.0/load"): - with open("/sys/devices/platform/gpu.0/load", "r") as f: + with open("/sys/devices/platform/gpu.0/load") as f: gpuload = float(f.readline()) / 10 results["gpu"] = f"{gpuload}%" else: @@ -790,10 +949,10 @@ def get_hailo_temps() -> dict[str, float]: return temps -def _go2rtc_arbitrary_exec_allowed() -> bool: +def is_go2rtc_arbitrary_exec_allowed() -> bool: """Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker secrets, or the Home Assistant add-on options file.""" - raw: Optional[str] = None + raw: str | None = None if "GO2RTC_ALLOW_ARBITRARY_EXEC" in os.environ: raw = os.environ.get("GO2RTC_ALLOW_ARBITRARY_EXEC") elif ( @@ -822,7 +981,7 @@ def is_restricted_go2rtc_source(stream_source: str) -> bool: and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set.""" if not stream_source.strip().startswith(("echo:", "expr:", "exec:")): return False - return not _go2rtc_arbitrary_exec_allowed() + return not is_go2rtc_arbitrary_exec_allowed() def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess: @@ -839,7 +998,7 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro else: format_entries = None - def run(rtsp_transport: Optional[str] = None) -> sp.CompletedProcess: + def run(rtsp_transport: str | None = None) -> sp.CompletedProcess: cmd = [ffmpeg.ffprobe_path] if rtsp_transport: cmd += ["-rtsp_transport", rtsp_transport] @@ -879,7 +1038,132 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro return result -def vainfo_hwaccel(device_name: Optional[str] = None) -> sp.CompletedProcess: +KEYFRAME_PROBE_WINDOW_SECONDS = 20 +KEYFRAME_GAP_WARNING_SECONDS = 4.0 + + +def parse_keyframe_packets(output: str) -> tuple[list[float], float | None]: + """Parse ffprobe CSV `pts_time,flags` output. + + Returns the presentation timestamps of keyframes (flags containing "K") + and the maximum timestamp observed across all packets. + """ + keyframe_pts: list[float] = [] + max_pts: float | None = None + + for line in output.splitlines(): + parts = line.split(",") + if len(parts) < 2: + continue + try: + pts = float(parts[0]) + except ValueError: + continue + if max_pts is None or pts > max_pts: + max_pts = pts + if "K" in parts[1]: + keyframe_pts.append(pts) + + return keyframe_pts, max_pts + + +def classify_keyframe_gaps( + keyframe_pts: list[float], segment_time: int +) -> dict[str, Any]: + """Classify keyframe spacing for recording suitability. + + A camera using a smart/+ codec or a long/variable GOP produces large or + irregular gaps between keyframes, which breaks time-based recording + segmentation. Severity: + - "unknown" when fewer than two keyframes were observed + - "error" when the longest gap exceeds the record segment length + - "warning" when the longest gap exceeds the warning threshold + - "ok" otherwise + """ + thresholds = { + "warning": KEYFRAME_GAP_WARNING_SECONDS, + "error": segment_time, + } + + if len(keyframe_pts) < 2: + return { + "keyframe_count": len(keyframe_pts), + "max_gap": None, + "mean_gap": None, + "min_gap": None, + "segment_time": segment_time, + "severity": "unknown", + "thresholds": thresholds, + } + + gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])] + max_gap = max(gaps) + + if max_gap > segment_time: + severity = "error" + elif max_gap > KEYFRAME_GAP_WARNING_SECONDS: + severity = "warning" + else: + severity = "ok" + + return { + "keyframe_count": len(keyframe_pts), + "max_gap": round(max_gap, 2), + "mean_gap": round(sum(gaps) / len(gaps), 2), + "min_gap": round(min(gaps), 2), + "segment_time": segment_time, + "severity": severity, + "thresholds": thresholds, + } + + +async def analyze_record_keyframes( + ffmpeg, url: str, segment_time: int, window: int = KEYFRAME_PROBE_WINDOW_SECONDS +) -> dict[str, Any]: + """Probe a stream for ~`window` seconds and classify its keyframe spacing. + + Reads video packet flags via ffprobe to find keyframes, then measures the + gaps between them. On timeout or failure returns an "unknown" result rather + than a false all-clear. + """ + clean_url = escape_special_characters(url) + cmd = [ + ffmpeg.ffprobe_path, + "-v", + "error", + "-select_streams", + "v:0", + "-read_intervals", + f"%+{window}", + "-show_entries", + "packet=pts_time,flags", + "-of", + "csv=p=0", + clean_url, + ] + + try: + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=window + 15) + except TimeoutError: + logger.warning("Keyframe probe timed out for record stream") + proc.kill() + return classify_keyframe_gaps([], segment_time) + except OSError as err: + logger.error("Keyframe probe failed: %s", err) + return classify_keyframe_gaps([], segment_time) + + keyframe_pts, max_pts = parse_keyframe_packets(stdout.decode("utf-8", "replace")) + result = classify_keyframe_gaps(keyframe_pts, segment_time) + result["duration_observed"] = round(max_pts, 2) if max_pts is not None else None + return result + + +def vainfo_hwaccel(device_name: str | None = None) -> sp.CompletedProcess: """Run vainfo.""" if not device_name: cmd = ["vainfo"] @@ -956,8 +1240,8 @@ async def get_video_properties( ) -> dict[str, Any]: async def probe_with_ffprobe( url: str, - rtsp_transport: Optional[str] = None, - ) -> tuple[bool, int, int, Optional[str], float]: + rtsp_transport: str | None = None, + ) -> tuple[bool, int, int, str | None, float]: """Fallback using ffprobe: returns (valid, width, height, codec, duration).""" cmd = [ffmpeg.ffprobe_path] if rtsp_transport: @@ -980,7 +1264,7 @@ async def get_video_properties( ) try: stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=6) - except asyncio.TimeoutError: + except TimeoutError: logger.info( "ffprobe timed out while probing %s (transport=%s)", clean_camera_user_pass(url), @@ -1012,7 +1296,7 @@ async def get_video_properties( except (json.JSONDecodeError, ValueError, KeyError, sp.SubprocessError): return False, 0, 0, None, -1 - def probe_with_cv2(url: str) -> tuple[bool, int, int, Optional[str], float]: + def probe_with_cv2(url: str) -> tuple[bool, int, int, str | None, float]: """Primary attempt using cv2: returns (valid, width, height, fourcc, duration).""" cap = cv2.VideoCapture(url) if not cap.isOpened(): @@ -1072,10 +1356,10 @@ async def get_video_properties( def process_logs( contents: str, - service: Optional[str] = None, - start: Optional[int] = None, - end: Optional[int] = None, -) -> Tuple[int, List[str]]: + service: str | None = None, + start: int | None = None, + end: int | None = None, +) -> tuple[int, list[str]]: log_lines = [] last_message = None last_timestamp = None diff --git a/frigate/util/time.py b/frigate/util/time.py index 1e7b49c243..861bec17f6 100644 --- a/frigate/util/time.py +++ b/frigate/util/time.py @@ -2,7 +2,6 @@ import datetime import logging -from typing import Tuple from zoneinfo import ZoneInfoNotFoundError import pytz @@ -11,7 +10,7 @@ from tzlocal import get_localzone logger = logging.getLogger(__name__) -def get_tz_modifiers(tz_name: str) -> Tuple[str, str, float]: +def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: seconds_offset = ( datetime.datetime.now(pytz.timezone(tz_name)).utcoffset().total_seconds() ) @@ -27,24 +26,18 @@ def get_tomorrow_at_time(hour: int) -> datetime.datetime: try: tomorrow = datetime.datetime.now(get_localzone()) + datetime.timedelta(days=1) except ZoneInfoNotFoundError: - tomorrow = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta( - days=1 - ) + tomorrow = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=1) logger.warning( "Using utc for maintenance due to missing or incorrect timezone set" ) - return tomorrow.replace(hour=hour, minute=0, second=0).astimezone( - datetime.timezone.utc - ) + return tomorrow.replace(hour=hour, minute=0, second=0).astimezone(datetime.UTC) def is_current_hour(timestamp: int) -> bool: """Returns if timestamp is in the current UTC hour.""" start_of_next_hour = ( - datetime.datetime.now(datetime.timezone.utc).replace( - minute=0, second=0, microsecond=0 - ) + datetime.datetime.now(datetime.UTC).replace(minute=0, second=0, microsecond=0) + datetime.timedelta(hours=1) ).timestamp() return timestamp < start_of_next_hour diff --git a/frigate/video/detect.py b/frigate/video/detect.py index 89124a75de..8c5a31e8df 100644 --- a/frigate/video/detect.py +++ b/frigate/video/detect.py @@ -3,7 +3,7 @@ import logging import queue import time -from datetime import datetime, timezone +from datetime import UTC, datetime from multiprocessing import Queue from multiprocessing.synchronize import Event as MpEvent from typing import Any @@ -273,7 +273,7 @@ def process_frames( time.sleep(0.1) continue - if datetime.now().astimezone(timezone.utc) > next_region_update: + if datetime.now().astimezone(UTC) > next_region_update: region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name) next_region_update = get_tomorrow_at_time(2) diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py index e77c03b5e5..24b7805333 100644 --- a/frigate/video/ffmpeg.py +++ b/frigate/video/ffmpeg.py @@ -6,7 +6,7 @@ import subprocess as sp import threading import time from collections import deque -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from multiprocessing import Queue, Value from multiprocessing.synchronize import Event as MpEvent from typing import Any @@ -24,7 +24,7 @@ from frigate.config.camera.updater import ( ) from frigate.const import PROCESS_PRIORITY_HIGH from frigate.log import LogPipe -from frigate.util.builtin import EventsPerSecond, get_ffmpeg_arg_list +from frigate.util.builtin import EventsPerSecond, get_record_segment_time from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg from frigate.util.image import ( FrameManager, @@ -34,23 +34,6 @@ from frigate.util.process import FrigateProcess logger = logging.getLogger(__name__) -# all built-in record presets use this segment_time -DEFAULT_RECORD_SEGMENT_TIME = 10 - - -def _get_record_segment_time(config: CameraConfig) -> int: - """Extract -segment_time from the camera's record output args.""" - record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record) - - if record_args and record_args[0].startswith("preset"): - return DEFAULT_RECORD_SEGMENT_TIME - - try: - idx = record_args.index("-segment_time") - return int(record_args[idx + 1]) - except (ValueError, IndexError): - return DEFAULT_RECORD_SEGMENT_TIME - def capture_frames( ffmpeg_process: sp.Popen[Any], @@ -185,7 +168,7 @@ class CameraWatchdog(threading.Thread): # `valid` segments are published with the segment's start time, so the # gap between consecutive publishes can reach 2 * segment_time. Pad the # staleness threshold so it's never tighter than that worst case. - segment_time = _get_record_segment_time(self.config) + segment_time = get_record_segment_time(self.config) self.record_stale_threshold = max(120, 2 * segment_time + 30) # Stall tracking (based on last processed frame) @@ -277,7 +260,7 @@ class CameraWatchdog(threading.Thread): self.start_all_ffmpeg() # If recording is enabled at startup, set the grace period timer if self.config.record.enabled: - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) time.sleep(self.sleeptime) last_restart_time = datetime.now().timestamp() @@ -297,7 +280,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) last_restart_time = datetime.now().timestamp() continue @@ -311,7 +294,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) else: self.logger.debug(f"Disabling camera {self.config.name}") self.stop_all_ffmpeg() @@ -335,7 +318,7 @@ class CameraWatchdog(threading.Thread): self.latest_valid_segment_time = 0 self.latest_invalid_segment_time = 0 self.latest_cache_segment_time = 0 - self.record_enable_time = datetime.now().astimezone(timezone.utc) + self.record_enable_time = datetime.now().astimezone(UTC) last_restart_time = datetime.now().timestamp() self.was_record_enabled_in_config = record_enabled_in_config continue @@ -419,7 +402,7 @@ class CameraWatchdog(threading.Thread): poll = p["process"].poll() if self.config.record.enabled and "record" in p["roles"]: - now_utc = datetime.now().astimezone(timezone.utc) + now_utc = datetime.now().astimezone(UTC) # Check if we're within the grace period after enabling recording # Grace period: 90 seconds allows time for ffmpeg to start and create first segment @@ -428,25 +411,19 @@ class CameraWatchdog(threading.Thread): ) < timedelta(seconds=90) latest_cache_dt = ( - datetime.fromtimestamp( - self.latest_cache_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_cache_segment_time, tz=UTC) if self.latest_cache_segment_time > 0 else now_utc - timedelta(seconds=1) ) latest_valid_dt = ( - datetime.fromtimestamp( - self.latest_valid_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_valid_segment_time, tz=UTC) if self.latest_valid_segment_time > 0 else now_utc - timedelta(seconds=1) ) latest_invalid_dt = ( - datetime.fromtimestamp( - self.latest_invalid_segment_time, tz=timezone.utc - ) + datetime.fromtimestamp(self.latest_invalid_segment_time, tz=UTC) if self.latest_invalid_segment_time > 0 else now_utc - timedelta(seconds=1) ) diff --git a/frigate/watchdog.py b/frigate/watchdog.py index 7ae42d9883..88023d5122 100644 --- a/frigate/watchdog.py +++ b/frigate/watchdog.py @@ -3,9 +3,9 @@ import logging import threading import time from collections import deque +from collections.abc import Callable from dataclasses import dataclass, field from multiprocessing.synchronize import Event as MpEvent -from typing import Callable from frigate.object_detection.base import ObjectDetectProcess from frigate.util.process import FrigateProcess diff --git a/generate_api_auth_spec.py b/generate_api_auth_spec.py new file mode 100644 index 0000000000..5768a6db64 --- /dev/null +++ b/generate_api_auth_spec.py @@ -0,0 +1,606 @@ +"""Generate the OpenAPI spec from the app, annotated with auth requirements. + +This generator builds the FastAPI application, exports its OpenAPI document via +``app.openapi()``, and enriches every operation with authentication metadata: + + * a ``components.securitySchemes`` block, + * a per-operation ``security`` requirement (so the docs render a lock badge), + * an ``x-required-role`` extension for machine readers, and + * a short bold ``Access:`` note prepended to each operation description. + +The committed docs/static/frigate-api.yaml is the output of this script. It is +generated rather than hand-maintained so it stays complete and current; the docs +build (docusaurus-plugin-openapi-docs) consumes it as-is. + +The access level for an endpoint is determined by BOTH its route-level +dependency (``require_role``/``allow_any_authenticated``/``allow_public``/ +``require_camera_access``) AND the global "secure by default" admin dependency, +which is bypassed only for the paths listed in ``require_admin_by_default``. +Those exempt lists are read directly from the function's closure so this script +stays in lockstep with ``frigate/api/auth.py`` instead of duplicating them. + +Many handlers enforce per-camera access by calling ``require_camera_access`` +inside the handler body rather than as a route dependency, which dependency +introspection cannot see. We recover those from the handler's bytecode (see +``_handler_enforces_camera``) and promote an otherwise "any authenticated" +operation to camera-scoped. + +Usage (from the repository root): + + python3 generate_api_auth_spec.py # write the spec + python3 generate_api_auth_spec.py --check # CI guard: fail if stale + +The process exits non-zero if the generated document fails structural +validation, or (in --check mode) if the committed spec is out of date. +""" + +import argparse +import difflib +import inspect +import io +import logging +import sys +from pathlib import Path + +from fastapi import FastAPI +from fastapi.routing import APIRoute +from ruamel.yaml import YAML +from ruamel.yaml.scalarstring import LiteralScalarString + +from frigate.api import app as main_app +from frigate.api import ( + auth, + camera, + chat, + classification, + debug_replay, + event, + export, + media, + motion_search, + notification, + preview, + record, + review, +) +from frigate.api.auth import require_admin_by_default + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger("generate_api_auth_spec") + +REPO_ROOT = Path(__file__).resolve().parent +OUTPUT_SPEC = REPO_ROOT / "docs" / "static" / "frigate-api.yaml" + +HTTP_METHODS = {"get", "post", "put", "delete", "patch"} + +# Banner written at the top of the generated spec. +HEADER = ( + "# Generated by generate_api_auth_spec.py — do not edit by hand.\n" + "# Regenerate with: python3 generate_api_auth_spec.py\n" + "# The empty info.title is intentional: a docusaurus-openapi-docs convention\n" + "# that suppresses the generated API introduction page.\n" +) + +# Post-processing applied on top of the raw app.openapi() export. These live +# only in the published spec, not in the app, so they are reproduced here. +SPEC_TITLE = "" +SPEC_SERVERS = [ + {"url": "https://demo.frigate.video/api"}, + {"url": "http://localhost:5001/api"}, +] + +# Access levels, ordered from least to most privileged. The string values are +# also what we emit as ``x-required-role``. +PUBLIC = "public" +AUTHENTICATED = "any" +CAMERA = "camera" +ADMIN = "admin" + +ADMIN_SCHEME = "frigateAdminAuth" +USER_SCHEME = "frigateUserAuth" + +SECURITY_SCHEMES = { + ADMIN_SCHEME: { + "type": "apiKey", + "in": "cookie", + "name": "frigate_token", + "description": ( + "Authenticated session whose resolved role is 'admin'. The session " + "is established via the JWT cookie issued by POST /login, or via " + "proxy auth headers (remote-user / remote-role) when Frigate runs " + "behind an authenticating reverse proxy." + ), + }, + USER_SCHEME: { + "type": "apiKey", + "in": "cookie", + "name": "frigate_token", + "description": ( + "Any authenticated session (role 'viewer' or higher), established " + "via the JWT cookie issued by POST /login, or via proxy auth " + "headers when Frigate runs behind an authenticating reverse proxy." + ), + }, +} + +# How each access level maps to a rendered note. +ACCESS_NOTES = { + PUBLIC: "**Access:** Public — no authentication required.", + AUTHENTICATED: "**Access:** Any authenticated user.", + CAMERA: "**Access:** Authenticated user with access to the referenced camera.", + ADMIN: "**Access:** Admin role required.", +} + + +def build_app() -> FastAPI: + """Build a bare app with every router mounted. + + This mirrors the router set wired up in frigate.api.fastapi_app. It omits + the global admin dependency and all runtime state; the OpenAPI route table + and the per-route dependencies are all we need to export and classify. + """ + app = FastAPI() + routers = [ + auth.router, + camera.router, + chat.router, + classification.router, + review.router, + main_app.router, + preview.router, + notification.router, + export.router, + event.router, + media.router, + motion_search.router, + record.router, + debug_replay.router, + ] + for router in routers: + app.include_router(router) + return app + + +def read_exempt_rules() -> tuple[set[str], tuple[str, ...]]: + """Read the admin-exemption lists straight from the auth dependency closure. + + Reading them here (rather than copying) keeps this generator in sync with + frigate/api/auth.py automatically. + """ + closure = inspect.getclosurevars(require_admin_by_default()).nonlocals + exempt_paths = set(closure["EXEMPT_PATHS"]) + exempt_prefixes = tuple(closure["EXEMPT_PREFIXES"]) + return exempt_paths, exempt_prefixes + + +def _first_segment(path: str) -> str: + return path.split("/", 2)[1] if path.startswith("/") and len(path) > 1 else "" + + +def _route_markers(route: APIRoute) -> tuple[set[str], list[str] | None]: + """Return the set of recognized auth markers on a route's dependencies.""" + markers: set[str] = set() + admin_roles: list[str] | None = None + + for dep in route.dependant.dependencies: + call = dep.call + qualname = getattr(call, "__qualname__", "") or "" + name = getattr(call, "__name__", "") or "" + + if "role_checker" in qualname: + markers.add(ADMIN) + try: + roles = inspect.getclosurevars(call).nonlocals.get("required_roles") + if roles: + admin_roles = list(roles) + except (TypeError, ValueError): + pass + elif name in ("require_camera_access", "require_go2rtc_stream_access"): + markers.add(CAMERA) + elif "auth_checker" in qualname: + markers.add(AUTHENTICATED) + elif "public_checker" in qualname: + markers.add(PUBLIC) + + return markers, admin_roles + + +def _handler_enforces_camera(route: APIRoute) -> bool: + """True if the route handler calls require_camera_access in its body. + + Such calls are invisible to dependency introspection. We detect them from + the handler's compiled bytecode: a global name referenced anywhere in the + function appears in ``__code__.co_names``. This catches direct calls (all of + them, currently); a call hidden behind a helper function would be missed. + """ + code = getattr(route.endpoint, "__code__", None) + return bool(code and "require_camera_access" in code.co_names) + + +def classify_route( + route: APIRoute, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> tuple[str, list[str] | None, str | None]: + """Resolve the effective access level for a route. + + Returns (access_level, roles, flag). ``flag`` is a human-readable note when + the result needed inference or revealed a possible inconsistency. + """ + level, roles, flag = _classify_base(route, exempt_paths, exempt_prefixes) + + # In-body require_camera_access enforcement is invisible to dependency + # introspection. When the effective access would otherwise be "any + # authenticated", the handler's per-camera check is the real constraint, so + # promote it to camera-scoped. Admin/public are left alone: for admin the + # role is the binding requirement and the camera check is only defensive. + if level == AUTHENTICATED and _handler_enforces_camera(route): + return CAMERA, None, None + + return level, roles, flag + + +def _classify_base( + route: APIRoute, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> tuple[str, list[str] | None, str | None]: + """Resolve the access level from route-level dependencies and exempt rules.""" + markers, admin_roles = _route_markers(route) + path = route.path + is_camera_path = _first_segment(path) == "{camera_name}" + exempt = path in exempt_paths or path.startswith(exempt_prefixes) or is_camera_path + + # Explicit route-level markers win, in order of specificity. + if ADMIN in markers: + return ADMIN, admin_roles or ["admin"], None + if CAMERA in markers: + return CAMERA, None, None + if AUTHENTICATED in markers: + if exempt: + return AUTHENTICATED, None, None + # The route opts in to any-authenticated, but the global admin check is + # not bypassed for this path, so admin is what actually gets enforced. + return ( + ADMIN, + ["admin"], + ( + "route declares allow_any_authenticated but path is not exempt from " + "the global admin check; admin is effectively enforced" + ), + ) + if PUBLIC in markers: + if exempt: + return PUBLIC, None, None + return ( + ADMIN, + ["admin"], + ( + "route declares allow_public but path is not exempt from the global " + "admin check; admin is effectively enforced" + ), + ) + + # No explicit auth marker: governed purely by the global default. + if not exempt: + return ADMIN, ["admin"], None + + # Exempt with no route dependency: the global admin check is bypassed and + # there is no route-level gate, so authorization (if any) happens inside the + # handler. Infer from the path shape and flag for confirmation. + if is_camera_path: + return ( + CAMERA, + None, + ( + "no route-level dependency; camera-scoped path, authorization " + "assumed to be enforced in the handler" + ), + ) + return ( + AUTHENTICATED, + None, + ( + "path is exempt from the global admin check but has no route-level " + "dependency; confirm authorization is enforced in the handler" + ), + ) + + +def build_access_map( + app: FastAPI, + exempt_paths: set[str], + exempt_prefixes: tuple[str, ...], +) -> dict[tuple[str, str], dict]: + """Map (path, lowercase method) -> classification details.""" + access_map: dict[tuple[str, str], dict] = {} + for route in app.routes: + if not isinstance(route, APIRoute): + continue + level, roles, flag = classify_route(route, exempt_paths, exempt_prefixes) + for method in route.methods: + if method in ("HEAD", "OPTIONS"): + continue + access_map[(route.path, method.lower())] = { + "level": level, + "roles": roles, + "flag": flag, + "path": route.path, + "method": method, + } + return access_map + + +def security_for(level: str) -> list: + """Build the OpenAPI ``security`` value for an access level.""" + if level == PUBLIC: + return [] + if level == ADMIN: + return [{ADMIN_SCHEME: []}] + # AUTHENTICATED and CAMERA both require any authenticated session; the + # camera-specific scoping is conveyed in the note and x-required-role. + return [{USER_SCHEME: []}] + + +def required_role_value(level: str, roles: list[str] | None): + if level == ADMIN and roles and roles != ["admin"]: + return roles + return level + + +def annotate_description(operation: dict, note: str) -> None: + existing = operation.get("description") + if not existing: + operation["description"] = note + return + operation["description"] = LiteralScalarString( + f"{note}\n\n{str(existing).rstrip()}" + ) + + +def base_document(raw: dict) -> dict: + """Apply the docs pipeline post-processing with a stable top-level order.""" + info = dict(raw.get("info", {})) + info["title"] = SPEC_TITLE + return { + "openapi": raw["openapi"], + "info": info, + "servers": [dict(server) for server in SPEC_SERVERS], + "paths": raw["paths"], + "components": raw.get("components", {}), + } + + +def enrich(spec: dict, access_map: dict) -> tuple[dict, list, list]: + """Add security schemes and per-operation auth metadata in place.""" + components = spec.setdefault("components", {}) + components["securitySchemes"] = dict(SECURITY_SCHEMES) + + counts: dict[str, int] = {} + flagged: list[dict] = [] + unmatched: list[tuple[str, str]] = [] + + for path, path_item in spec["paths"].items(): + for method, operation in path_item.items(): + if method.lower() not in HTTP_METHODS: + continue + details = access_map.get((path, method.lower())) + if details is None: + unmatched.append((method.upper(), path)) + continue + + level = details["level"] + counts[level] = counts.get(level, 0) + 1 + operation["security"] = security_for(level) + operation["x-required-role"] = required_role_value(level, details["roles"]) + annotate_description(operation, ACCESS_NOTES[level]) + + if details["flag"]: + flagged.append(details) + + return counts, flagged, unmatched + + +# Numeric defaults at or above this magnitude are treated as live Unix +# timestamps baked into the schema at import time (e.g. the /{camera_name} +# /recordings after/before params default to datetime.now()). They make the +# export non-deterministic and document a meaningless frozen epoch, so they are +# stripped. The proper fix is to default those route params to None and resolve +# "now" inside the handler. +VOLATILE_DEFAULT_THRESHOLD = 1_000_000_000 + + +def strip_volatile_defaults(node, trail: str = "") -> list[tuple[str, float]]: + """Remove epoch-like numeric ``default`` values so the export is stable. + + Returns the (location, value) pairs that were removed, for reporting. + """ + removed: list[tuple[str, float]] = [] + if isinstance(node, dict): + default = node.get("default") + if ( + isinstance(default, (int, float)) + and not isinstance(default, bool) + and default >= VOLATILE_DEFAULT_THRESHOLD + ): + removed.append((trail, default)) + del node["default"] + for key, value in node.items(): + removed.extend(strip_volatile_defaults(value, f"{trail}/{key}")) + elif isinstance(node, list): + for index, value in enumerate(node): + removed.extend(strip_volatile_defaults(value, f"{trail}[{index}]")) + return removed + + +def to_block_scalars(node): + """Recursively render multi-line strings as literal block scalars. + + Produces readable, deterministic YAML (``|-`` blocks) instead of long + double-quoted lines with escaped newlines. + """ + if isinstance(node, dict): + return {key: to_block_scalars(value) for key, value in node.items()} + if isinstance(node, list): + return [to_block_scalars(value) for value in node] + if isinstance(node, str) and "\n" in node: + return LiteralScalarString(node) + return node + + +def _iter_refs(node): + if isinstance(node, dict): + for key, value in node.items(): + if key == "$ref" and isinstance(value, str): + yield value + else: + yield from _iter_refs(value) + elif isinstance(node, list): + for value in node: + yield from _iter_refs(value) + + +def validate(spec: dict) -> list[str]: + """Structural sanity checks on the generated document.""" + problems: list[str] = [] + schemas = set(spec.get("components", {}).get("schemas", {})) + defined_schemes = set(spec.get("components", {}).get("securitySchemes", {})) + + for ref in _iter_refs(spec): + if ref.startswith("#/components/schemas/"): + name = ref.rsplit("/", 1)[-1] + if name not in schemas: + problems.append(f"dangling $ref: {ref}") + + for path, path_item in spec.get("paths", {}).items(): + for method, operation in path_item.items(): + if method.lower() not in HTTP_METHODS or not isinstance(operation, dict): + continue + location = f"{method.upper()} {path}" + if "x-required-role" not in operation: + problems.append(f"missing x-required-role: {location}") + if "security" not in operation: + problems.append(f"missing security: {location}") + continue + for requirement in operation["security"]: + for scheme in requirement: + if scheme not in defined_schemes: + problems.append( + f"undefined security scheme {scheme}: {location}" + ) + + return sorted(set(problems)) + + +def render(spec: dict) -> str: + """Serialize the spec to the canonical YAML string (with the header).""" + yaml = YAML() + yaml.width = 80 + yaml.indent(mapping=2, sequence=4, offset=2) + stream = io.StringIO() + yaml.dump(spec, stream) + return HEADER + stream.getvalue() + + +def build_spec() -> tuple[dict, dict, list, list, list]: + app = build_app() + exempt_paths, exempt_prefixes = read_exempt_rules() + access_map = build_access_map(app, exempt_paths, exempt_prefixes) + + spec = base_document(app.openapi()) + normalized = strip_volatile_defaults(spec) + counts, flagged, unmatched = enrich(spec, access_map) + spec = to_block_scalars(spec) + return spec, counts, flagged, unmatched, normalized + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Generate the annotated OpenAPI spec.") + parser.add_argument( + "--check", + action="store_true", + help="verify the committed spec is up to date without writing; " + "exit non-zero if it would change", + ) + args = parser.parse_args(argv) + + spec, counts, flagged, unmatched, normalized = build_spec() + problems = validate(spec) + rendered = render(spec) + + if args.check: + return _check(rendered, problems) + + if problems: + logger.error("Refusing to write — generated spec failed validation:") + for problem in problems: + logger.error(" %s", problem) + return 1 + + OUTPUT_SPEC.write_text(rendered) + _report(counts, flagged, unmatched, normalized) + logger.info("\nWrote %s", OUTPUT_SPEC.relative_to(REPO_ROOT)) + return 0 + + +def _check(rendered: str, problems: list[str]) -> int: + name = OUTPUT_SPEC.relative_to(REPO_ROOT) + if problems: + logger.error("Generated spec failed validation:") + for problem in problems: + logger.error(" %s", problem) + return 1 + + current = OUTPUT_SPEC.read_text() if OUTPUT_SPEC.exists() else "" + if current == rendered: + logger.info("%s is up to date", name) + return 0 + + logger.error( + "%s is out of date. Regenerate with: python3 %s", + name, + Path(__file__).name, + ) + diff = difflib.unified_diff( + current.splitlines(), + rendered.splitlines(), + fromfile=f"{name} (committed)", + tofile=f"{name} (generated)", + lineterm="", + n=2, + ) + for shown, line in enumerate(diff): + if shown >= 60: + logger.error(" ... (diff truncated)") + break + logger.error(" %s", line) + return 1 + + +def _report(counts, flagged, unmatched, normalized) -> None: + logger.info("Access levels applied:") + for level in (PUBLIC, AUTHENTICATED, CAMERA, ADMIN): + logger.info(" %-14s %d", level, counts.get(level, 0)) + logger.info(" %-14s %d", "total", sum(counts.values())) + + if normalized: + logger.info("\nStripped volatile timestamp defaults (%d):", len(normalized)) + for location, value in normalized: + logger.info(" %s = %s", location.lstrip("/"), value) + + if flagged: + logger.info("\nFlagged for manual confirmation (%d):", len(flagged)) + for item in flagged: + logger.info(" %-6s %s", item["method"], item["path"]) + logger.info(" -> %s (%s)", item["level"], item["flag"]) + + if unmatched: + logger.info( + "\nOperations with no classification (%d) [unexpected]:", len(unmatched) + ) + for method, path in unmatched: + logger.info(" %-6s %s", method, path) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/generate_config_translations.py b/generate_config_translations.py index 7f9c9bc504..aa115e44c3 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -10,7 +10,7 @@ import json import logging import sys from pathlib import Path -from typing import Any, Dict, get_args, get_origin +from typing import Any, get_args, get_origin from frigate.config.config import FrigateConfig from frigate.util.schema import get_config_schema @@ -19,7 +19,7 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def get_field_translations(field_info) -> Dict[str, str]: +def get_field_translations(field_info) -> dict[str, str]: """Extract title and description from a Pydantic field.""" translations = {} @@ -33,8 +33,8 @@ def get_field_translations(field_info) -> Dict[str, str]: def extract_translations_from_schema( - schema: Dict[str, Any], defs: Dict[str, Any] = None -) -> Dict[str, Any]: + schema: dict[str, Any], defs: dict[str, Any] = None +) -> dict[str, Any]: """ Recursively extract translations (titles and descriptions) from a JSON schema. @@ -202,7 +202,7 @@ def extract_translations_from_schema( return translations -def generate_section_translation(config_class: type) -> Dict[str, Any]: +def generate_section_translation(config_class: type) -> dict[str, Any]: """ Generate translation structure for a config section using its JSON schema. """ @@ -211,8 +211,8 @@ def generate_section_translation(config_class: type) -> Dict[str, Any]: def get_detector_translations( - config_schema: Dict[str, Any], -) -> tuple[Dict[str, Any], Dict[str, Any], set[str]]: + config_schema: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], set[str]]: """Build detector type translations with nested fields based on schema definitions. Returns a tuple of (type_translations, shared_fields, nested_field_keys). @@ -225,8 +225,8 @@ def get_detector_translations( mapping = discriminator.get("mapping", {}) # First pass: collect all nested fields per detector type - all_nested: Dict[str, Dict[str, Any]] = {} - type_meta: Dict[str, Dict[str, str]] = {} + all_nested: dict[str, dict[str, Any]] = {} + type_meta: dict[str, dict[str, str]] = {} for detector_type, ref in mapping.items(): if not isinstance(ref, str) or not ref.startswith("#/$defs/"): @@ -237,7 +237,7 @@ def get_detector_translations( if not ref_schema: continue - meta: Dict[str, str] = {} + meta: dict[str, str] = {} title = ref_schema.get("title") description = ref_schema.get("description") if title: @@ -252,7 +252,7 @@ def get_detector_translations( } # Find fields that are identical across all types that have them - shared_fields: Dict[str, Any] = {} + shared_fields: dict[str, Any] = {} if all_nested: # Collect all field keys across all types all_keys: set[str] = set() @@ -265,10 +265,10 @@ def get_detector_translations( shared_fields[key] = values[0] # Build per-type translations with only unique (non-shared) fields - type_translations: Dict[str, Any] = {} + type_translations: dict[str, Any] = {} nested_field_keys: set[str] = set() for detector_type, nested in all_nested.items(): - type_entry: Dict[str, Any] = {} + type_entry: dict[str, Any] = {} type_entry.update(type_meta.get(detector_type, {})) unique_fields = {k: v for k, v in nested.items() if k not in shared_fields} @@ -387,7 +387,7 @@ def main(): # doesn't capture the attribute-specific semantics. Keys # match the FilterConfig field name; values are partial # overrides applied AFTER the generic rewrites. - attribute_field_overrides: Dict[str, Dict[str, str]] = { + attribute_field_overrides: dict[str, dict[str, str]] = { "min_score": { "description": ( "Minimum single-frame detection confidence required " @@ -401,7 +401,7 @@ def main(): text = text.replace(source, replacement) return text - attribute_variant: Dict[str, Any] = {} + attribute_variant: dict[str, Any] = {} for key, value in filters_block.items(): if key in ("label", "description"): if isinstance(value, str): @@ -409,7 +409,7 @@ def main(): continue if not isinstance(value, dict): continue - field_trans: Dict[str, str] = {} + field_trans: dict[str, str] = {} if isinstance(value.get("label"), str): field_trans["label"] = rewrite(value["label"]) if isinstance(value.get("description"), str): diff --git a/pyproject.toml b/pyproject.toml index d17a60e726..775db6026c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[tool.ruff] +target-version = "py311" + [tool.ruff.lint] -ignore = ["E501","E711","E712"] -extend-select = ["I"] +ignore = ["E501","E711","E712","UP031","UP032","UP042","G004"] +extend-select = ["I", "UP", "G", "ASYNC210", "B904"] diff --git a/web/.gitignore b/web/.gitignore index 1cac5597ea..ca98c7b96a 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -12,6 +12,10 @@ dist dist-ssr *.local +# Playwright +playwright-report +test-results + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/web/e2e/fixtures/mock-data/cases.json b/web/e2e/fixtures/mock-data/cases.json index 5d0c96b8c1..6174cdebf3 100644 --- a/web/e2e/fixtures/mock-data/cases.json +++ b/web/e2e/fixtures/mock-data/cases.json @@ -1 +1 @@ -[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1775407931.3863528, "updated_at": 1775483531.3863528}] \ No newline at end of file +[{"id": "case-001", "name": "Package Theft Investigation", "description": "Review of suspicious activity near the front porch", "created_at": 1780597809.365581, "updated_at": 1780673409.365581}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config-schema.json b/web/e2e/fixtures/mock-data/config-schema.json new file mode 100644 index 0000000000..04d72b1e5a --- /dev/null +++ b/web/e2e/fixtures/mock-data/config-schema.json @@ -0,0 +1 @@ +{"$defs": {"AlertsConfig": {"additionalProperties": false, "description": "Configure alerts", "properties": {"enabled": {"default": true, "description": "Enable or disable alert generation for all cameras; can be overridden per-camera.", "title": "Enable alerts", "type": "boolean"}, "labels": {"default": ["person", "car"], "description": "List of object labels that qualify as alerts (for example: car, person).", "items": {"type": "string"}, "title": "Alert labels", "type": "array"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered an alert; leave empty to allow any zone.", "title": "Required zones"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether alerts were originally enabled in the static configuration.", "title": "Original alerts state"}, "cutoff_time": {"default": 40, "description": "Seconds to wait after no alert-causing activity before cutting off an alert.", "title": "Alerts cutoff time", "type": "integer"}}, "title": "AlertsConfig", "type": "object"}, "AudioConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable audio event detection for all cameras; can be overridden per-camera.", "title": "Enable audio detection", "type": "boolean"}, "max_not_heard": {"default": 30, "description": "Amount of seconds without the configured audio type before the audio event is ended.", "title": "End timeout", "type": "integer"}, "min_volume": {"default": 500, "description": "Minimum RMS volume threshold required to run audio detection; lower values increase sensitivity (e.g., 200 high, 500 medium, 1000 low).", "title": "Minimum volume", "type": "integer"}, "listen": {"default": ["bark", "fire_alarm", "speech", "yell"], "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell).", "items": {"type": "string"}, "title": "Listen types", "type": "array"}, "filters": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/AudioFilterConfig"}, "type": "object"}, {"type": "null"}], "default": null, "description": "Per-audio-type filter settings such as confidence thresholds used to reduce false positives.", "title": "Audio filters"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether audio detection was originally enabled in the static config file.", "title": "Original audio state"}, "num_threads": {"default": 2, "description": "Number of threads to use for audio detection processing.", "minimum": 1, "title": "Detection threads", "type": "integer"}}, "title": "AudioConfig", "type": "object"}, "AudioFilterConfig": {"additionalProperties": false, "properties": {"threshold": {"default": 0.8, "description": "Minimum confidence threshold for the audio event to be counted.", "exclusiveMaximum": 1.0, "minimum": 0.5, "title": "Minimum audio confidence", "type": "number"}}, "title": "AudioFilterConfig", "type": "object"}, "AudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic audio transcription for all cameras; can be overridden per-camera.", "title": "Enable audio transcription", "type": "boolean"}, "language": {"default": "en", "description": "Language code used for transcription/translation (for example 'en' for English). See https://whisper-api.com/docs/languages/ for supported language codes.", "title": "Transcription language", "type": "string"}, "device": {"$ref": "#/$defs/EnrichmentsDeviceEnum", "default": "CPU", "description": "Device key (CPU/GPU) to run the transcription model on. Only NVIDIA CUDA GPUs are currently supported for transcription.", "title": "Transcription device"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for offline audio event transcription.", "title": "Model size"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "AudioTranscriptionConfig", "type": "object"}, "AuthConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable native authentication for the Frigate UI.", "title": "Enable authentication", "type": "boolean"}, "reset_admin_password": {"default": false, "description": "If true, reset the admin user's password on startup and print the new password in logs.", "title": "Reset admin password", "type": "boolean"}, "cookie_name": {"default": "frigate_token", "description": "Name of the cookie used to store the JWT token for native authentication.", "pattern": "^[a-z_]+$", "title": "JWT cookie name", "type": "string"}, "cookie_secure": {"default": false, "description": "Set the secure flag on the auth cookie; should be true when using TLS.", "title": "Secure cookie flag", "type": "boolean"}, "session_length": {"default": 86400, "description": "Session duration in seconds for JWT-based sessions.", "minimum": 60, "title": "Session length", "type": "integer"}, "refresh_time": {"default": 1800, "description": "When a session is within this many seconds of expiring, refresh it back to full length.", "minimum": 30, "title": "Session refresh window", "type": "integer"}, "failed_login_rate_limit": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Rate limiting rules for failed login attempts to reduce brute-force attacks.", "title": "Failed login limits"}, "trusted_proxies": {"default": [], "description": "List of trusted proxy IPs used when determining client IP for rate limiting.", "items": {"type": "string"}, "title": "Trusted proxies", "type": "array"}, "hash_iterations": {"default": 600000, "description": "Number of PBKDF2-SHA256 iterations to use when hashing user passwords.", "title": "Hash iterations", "type": "integer"}, "roles": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "description": "Map roles to camera lists. An empty list grants access to all cameras for the role.", "title": "Role mappings", "type": "object"}, "admin_first_time_login": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "When true the UI may show a help link on the login page informing users how to sign in after an admin password reset. ", "title": "First-time admin flag"}}, "title": "AuthConfig", "type": "object"}, "BaseDetectorConfig": {"additionalProperties": true, "properties": {"type": {"default": "cpu", "description": "Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').", "title": "Detector Type", "type": "string"}, "model": {"anyOf": [{"$ref": "#/$defs/ModelConfig"}, {"type": "null"}], "default": null, "description": "Detector-specific model configuration options (path, input size, etc.).", "title": "Detector specific model configuration"}, "model_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "File path to the detector model binary if required by the chosen detector.", "title": "Detector specific model path"}}, "title": "BaseDetectorConfig", "type": "object"}, "BirdClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable bird classification.", "title": "Bird classification", "type": "boolean"}, "threshold": {"default": 0.9, "description": "Minimum classification score required to accept a bird classification.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Minimum score", "type": "number"}}, "title": "BirdClassificationConfig", "type": "object"}, "BirdseyeCameraConfig": {"properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "order": {"default": 0, "description": "Numeric position controlling the camera's ordering in the Birdseye layout.", "title": "Position", "type": "integer"}}, "title": "BirdseyeCameraConfig", "type": "object"}, "BirdseyeConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the Birdseye view feature.", "title": "Enable Birdseye", "type": "boolean"}, "mode": {"$ref": "#/$defs/BirdseyeModeEnum", "default": "objects", "description": "Mode for including cameras in Birdseye: 'objects', 'motion', or 'continuous'.", "title": "Tracking mode"}, "restream": {"default": false, "description": "Re-stream the Birdseye output as an RTSP feed; enabling this will keep Birdseye running continuously.", "title": "Restream RTSP", "type": "boolean"}, "width": {"default": 1280, "description": "Output width (pixels) of the composed Birdseye frame.", "title": "Width", "type": "integer"}, "height": {"default": 720, "description": "Output height (pixels) of the composed Birdseye frame.", "title": "Height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the Birdseye mpeg1 feed (1 highest quality, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Encoding quality", "type": "integer"}, "inactivity_threshold": {"default": 30, "description": "Seconds of inactivity after which a camera will stop being shown in Birdseye.", "exclusiveMinimum": 0, "title": "Inactivity threshold", "type": "integer"}, "layout": {"$ref": "#/$defs/BirdseyeLayoutConfig", "description": "Layout options for the Birdseye composition.", "title": "Layout"}, "idle_heartbeat_fps": {"default": 0.0, "description": "Frames-per-second to resend the last composed Birdseye frame when idle; set to 0 to disable.", "maximum": 10.0, "minimum": 0.0, "title": "Idle heartbeat FPS", "type": "number"}}, "title": "BirdseyeConfig", "type": "object"}, "BirdseyeLayoutConfig": {"additionalProperties": false, "properties": {"scaling_factor": {"default": 2.0, "description": "Scaling factor used by the layout calculator (range 1.0 to 5.0).", "maximum": 5.0, "minimum": 1.0, "title": "Scaling factor", "type": "number"}, "max_cameras": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.", "title": "Max cameras"}}, "title": "BirdseyeLayoutConfig", "type": "object"}, "BirdseyeModeEnum": {"enum": ["objects", "motion", "continuous"], "title": "BirdseyeModeEnum", "type": "string"}, "CameraAudioTranscriptionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable manually triggered audio event transcription.", "title": "Enable transcription", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Original transcription state"}, "live_enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Enable streaming live transcription for audio as it is received.", "title": "Live transcription"}}, "title": "CameraAudioTranscriptionConfig", "type": "object"}, "CameraConfig": {"additionalProperties": false, "properties": {"name": {"anyOf": [{"pattern": "^[a-zA-Z0-9_-]+$", "type": "string"}, {"type": "null"}], "default": null, "description": "Camera name is required", "title": "Camera name"}, "friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Camera friendly name used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enabled", "title": "Enabled", "type": "boolean"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for this camera.", "title": "Audio detection"}, "audio_transcription": {"$ref": "#/$defs/CameraAudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "birdseye": {"$ref": "#/$defs/BirdseyeCameraConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "face_recognition": {"$ref": "#/$defs/CameraFaceRecognitionConfig", "description": "Settings for face detection and recognition for this camera.", "title": "Face recognition"}, "ffmpeg": {"$ref": "#/$defs/CameraFfmpegConfig", "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", "title": "Streams (FFmpeg)"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings used by the Web UI to control live stream selection, resolution and quality.", "title": "Live playback"}, "lpr": {"$ref": "#/$defs/CameraLicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "motion": {"$ref": "#/$defs/MotionConfig", "default": null, "description": "Default motion detection settings for this camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings for this camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage for this camera.", "title": "Review"}, "semantic_search": {"$ref": "#/$defs/CameraSemanticSearchConfig", "description": "Settings for semantic search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for this camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for timestamps applied to snapshots and Debug view.", "title": "Timestamp style"}, "best_image_timeout": {"default": 60, "description": "How long to wait for the image with the highest confidence score.", "title": "Best image timeout", "type": "integer"}, "mqtt": {"$ref": "#/$defs/CameraMqttConfig", "description": "MQTT image publishing settings.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for this camera.", "title": "Notifications"}, "onvif": {"$ref": "#/$defs/OnvifConfig", "description": "ONVIF connection and PTZ autotracking settings for this camera.", "title": "ONVIF"}, "type": {"$ref": "#/$defs/CameraTypeEnum", "default": "generic", "description": "Camera Type", "title": "Camera type"}, "ui": {"$ref": "#/$defs/CameraUiConfig", "description": "Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.", "title": "Camera UI"}, "webui_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to visit the camera directly from system page", "title": "Camera URL"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/CameraProfileConfig"}, "description": "Named config profiles with partial overrides that can be activated at runtime.", "title": "Profiles", "type": "object"}, "zones": {"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "description": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", "title": "Zones", "type": "object"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Keep track of original state of camera.", "title": "Original camera state"}}, "required": ["ffmpeg"], "title": "CameraConfig", "type": "object"}, "CameraFaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition.", "title": "Enable face recognition", "type": "boolean"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}}, "title": "CameraFaceRecognitionConfig", "type": "object"}, "CameraFfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}, "inputs": {"description": "List of input stream definitions (paths and roles) for this camera.", "items": {"$ref": "#/$defs/CameraInput"}, "title": "Camera inputs", "type": "array"}}, "required": ["inputs"], "title": "CameraFfmpegConfig", "type": "object"}, "CameraGroupConfig": {"additionalProperties": false, "properties": {"cameras": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Array of camera names included in this group.", "title": "Camera list"}, "icon": {"default": "generic", "description": "Icon used to represent the camera group in the UI.", "title": "Group icon", "type": "string"}, "order": {"default": 0, "description": "Numeric order used to sort camera groups in the UI; larger numbers appear later.", "title": "Sort order", "type": "integer"}}, "title": "CameraGroupConfig", "type": "object"}, "CameraInput": {"additionalProperties": false, "properties": {"path": {"description": "Camera input stream URL or path.", "title": "Input path", "type": "string"}, "roles": {"description": "Roles for this input stream.", "items": {"$ref": "#/$defs/CameraRoleEnum"}, "title": "Input roles", "type": "array"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "FFmpeg global arguments for this input stream.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Hardware acceleration arguments for this input stream.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Input arguments specific to this stream.", "title": "Input arguments"}}, "required": ["path", "roles"], "title": "CameraInput", "type": "object"}, "CameraLicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable LPR on this camera.", "title": "Enable LPR", "type": "boolean"}, "expire_time": {"default": 3, "description": "Time in seconds after which an unseen plate is expired from the tracker (for dedicated LPR cameras only).", "exclusiveMinimum": 0, "title": "Expire seconds", "type": "integer"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}}, "title": "CameraLicensePlateRecognitionConfig", "type": "object"}, "CameraLiveConfig": {"additionalProperties": false, "properties": {"streams": {"additionalProperties": {"type": "string"}, "description": "Mapping of configured stream names to restream/go2rtc names used for live playback.", "title": "Live stream names", "type": "object"}, "height": {"default": 720, "description": "Height (pixels) to render the jsmpeg live stream in the Web UI; must be <= detect stream height.", "title": "Live height", "type": "integer"}, "quality": {"default": 8, "description": "Encoding quality for the jsmpeg stream (1 highest, 31 lowest).", "maximum": 31, "minimum": 1, "title": "Live quality", "type": "integer"}}, "title": "CameraLiveConfig", "type": "object"}, "CameraMqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable publishing image snapshots for objects to MQTT topics for this camera.", "title": "Send image", "type": "boolean"}, "timestamp": {"default": true, "description": "Overlay a timestamp on images published to MQTT.", "title": "Add timestamp", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes on images published over MQTT.", "title": "Add bounding box", "type": "boolean"}, "crop": {"default": true, "description": "Crop images published to MQTT to the detected object's bounding box.", "title": "Crop image", "type": "boolean"}, "height": {"default": 270, "description": "Height (pixels) to resize images published over MQTT.", "title": "Image height", "type": "integer"}, "required_zones": {"description": "Zones that an object must enter for an MQTT image to be published.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "quality": {"default": 70, "description": "JPEG quality for images published to MQTT (0-100).", "maximum": 100, "minimum": 0, "title": "JPEG quality", "type": "integer"}}, "title": "CameraMqttConfig", "type": "object"}, "CameraProfileConfig": {"additionalProperties": false, "description": "A named profile containing partial camera config overrides.\n\nSections set to None inherit from the camera's base config.\nSections that are defined get Pydantic-validated, then only\nexplicitly-set fields are used as overrides via exclude_unset.", "properties": {"enabled": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Enabled"}, "audio": {"anyOf": [{"$ref": "#/$defs/AudioConfig"}, {"type": "null"}], "default": null}, "birdseye": {"anyOf": [{"$ref": "#/$defs/BirdseyeCameraConfig"}, {"type": "null"}], "default": null}, "detect": {"anyOf": [{"$ref": "#/$defs/DetectConfig"}, {"type": "null"}], "default": null}, "face_recognition": {"anyOf": [{"$ref": "#/$defs/CameraFaceRecognitionConfig"}, {"type": "null"}], "default": null}, "lpr": {"anyOf": [{"$ref": "#/$defs/CameraLicensePlateRecognitionConfig"}, {"type": "null"}], "default": null}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null}, "notifications": {"anyOf": [{"$ref": "#/$defs/NotificationConfig"}, {"type": "null"}], "default": null}, "objects": {"anyOf": [{"$ref": "#/$defs/ObjectConfig"}, {"type": "null"}], "default": null}, "record": {"anyOf": [{"$ref": "#/$defs/RecordConfig"}, {"type": "null"}], "default": null}, "review": {"anyOf": [{"$ref": "#/$defs/ReviewConfig"}, {"type": "null"}], "default": null}, "snapshots": {"anyOf": [{"$ref": "#/$defs/SnapshotsConfig"}, {"type": "null"}], "default": null}, "zones": {"anyOf": [{"additionalProperties": {"$ref": "#/$defs/ZoneConfig"}, "type": "object"}, {"type": "null"}], "default": null, "title": "Zones"}}, "title": "CameraProfileConfig", "type": "object"}, "CameraRoleEnum": {"enum": ["audio", "record", "detect"], "title": "CameraRoleEnum", "type": "string"}, "CameraSemanticSearchConfig": {"additionalProperties": false, "properties": {"triggers": {"additionalProperties": {"$ref": "#/$defs/TriggerConfig"}, "default": {}, "description": "Actions and matching criteria for camera-specific semantic search triggers.", "title": "Triggers", "type": "object"}}, "title": "CameraSemanticSearchConfig", "type": "object"}, "CameraTypeEnum": {"enum": ["generic", "lpr"], "title": "CameraTypeEnum", "type": "string"}, "CameraUiConfig": {"additionalProperties": false, "properties": {"order": {"default": 0, "description": "Numeric order used to sort the camera in the UI (default dashboard and lists); larger numbers appear later.", "title": "UI order", "type": "integer"}, "dashboard": {"default": true, "description": "Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.", "title": "Show in UI", "type": "boolean"}, "review": {"default": true, "description": "Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).", "title": "Show in review", "type": "boolean"}}, "title": "CameraUiConfig", "type": "object"}, "ClassificationConfig": {"additionalProperties": false, "properties": {"bird": {"$ref": "#/$defs/BirdClassificationConfig", "description": "Settings specific to bird classification models.", "title": "Bird classification config"}, "custom": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationConfig"}, "default": {}, "description": "Configuration for custom classification models used for objects or state detection.", "title": "Custom Classification Models", "type": "object"}}, "title": "ClassificationConfig", "type": "object"}, "ColorConfig": {"additionalProperties": false, "properties": {"red": {"default": 255, "description": "Red component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Red", "type": "integer"}, "green": {"default": 255, "description": "Green component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Green", "type": "integer"}, "blue": {"default": 255, "description": "Blue component (0-255) for timestamp color.", "maximum": 255, "minimum": 0, "title": "Blue", "type": "integer"}}, "title": "ColorConfig", "type": "object"}, "CustomClassificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable the custom classification model.", "title": "Enable model", "type": "boolean"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Identifier for the custom classification model to use.", "title": "Model name"}, "threshold": {"default": 0.8, "description": "Score threshold used to change the classification state.", "title": "Score threshold", "type": "number"}, "save_attempts": {"anyOf": [{"minimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How many classification attempts to save for recent classifications UI.", "title": "Save attempts"}, "object_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationObjectConfig"}, {"type": "null"}], "default": null}, "state_config": {"anyOf": [{"$ref": "#/$defs/CustomClassificationStateConfig"}, {"type": "null"}], "default": null}}, "title": "CustomClassificationConfig", "type": "object"}, "CustomClassificationObjectConfig": {"additionalProperties": false, "properties": {"objects": {"description": "List of object types to run object classification on.", "items": {"type": "string"}, "title": "Classify objects", "type": "array"}, "classification_type": {"$ref": "#/$defs/ObjectClassificationType", "default": "sub_label", "description": "Classification type applied: 'sub_label' (adds sub_label) or other supported types.", "title": "Classification type"}}, "title": "CustomClassificationObjectConfig", "type": "object"}, "CustomClassificationStateCameraConfig": {"additionalProperties": false, "properties": {"crop": {"description": "Crop coordinates to use for running classification on this camera.", "items": {"type": "number"}, "title": "Classification crop", "type": "array"}}, "required": ["crop"], "title": "CustomClassificationStateCameraConfig", "type": "object"}, "CustomClassificationStateConfig": {"additionalProperties": false, "properties": {"cameras": {"additionalProperties": {"$ref": "#/$defs/CustomClassificationStateCameraConfig"}, "description": "Per-camera crop and settings for running state classification.", "title": "Classification cameras", "type": "object"}, "motion": {"default": false, "description": "If true, run classification when motion is detected within the specified crop.", "title": "Run on motion", "type": "boolean"}, "interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "Interval (seconds) between periodic classification runs for state classification.", "title": "Classification interval"}}, "required": ["cameras"], "title": "CustomClassificationStateConfig", "type": "object"}, "DatabaseConfig": {"additionalProperties": false, "properties": {"path": {"default": "/config/frigate.db", "description": "Filesystem path where the Frigate SQLite database file will be stored.", "title": "Database path", "type": "string"}}, "title": "DatabaseConfig", "type": "object"}, "DetectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable object detection for all cameras; can be overridden per-camera.", "title": "Enable object detection", "type": "boolean"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect height"}, "width": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.", "title": "Detect width"}, "fps": {"default": 5, "description": "Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).", "title": "Detect FPS", "type": "integer"}, "min_initialized": {"anyOf": [{"minimum": 2, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.", "title": "Minimum initialization frames"}, "max_disappeared": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames without a detection before a tracked object is considered gone.", "title": "Maximum disappeared frames"}, "stationary": {"$ref": "#/$defs/StationaryConfig", "description": "Settings to detect and manage objects that remain stationary for a period of time.", "title": "Stationary objects config"}, "annotation_offset": {"default": 0, "description": "Milliseconds to shift detect annotations to better align timeline bounding boxes with recordings; can be positive or negative.", "title": "Annotation offset", "type": "integer"}}, "title": "DetectConfig", "type": "object"}, "DetectionsConfig": {"additionalProperties": false, "description": "Configure detections", "properties": {"enabled": {"default": true, "description": "Enable or disable detection events for all cameras; can be overridden per-camera.", "title": "Enable detections", "type": "boolean"}, "labels": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "description": "List of object labels that qualify as detection events.", "title": "Detection labels"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that an object must enter to be considered a detection; leave empty to allow any zone.", "title": "Required zones"}, "cutoff_time": {"default": 30, "description": "Seconds to wait after no detection-causing activity before cutting off a detection.", "title": "Detections cutoff time", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether detections were originally enabled in the static configuration.", "title": "Original detections state"}}, "title": "DetectionsConfig", "type": "object"}, "EnrichmentsDeviceEnum": {"enum": ["GPU", "CPU"], "title": "EnrichmentsDeviceEnum", "type": "string"}, "EventsConfig": {"additionalProperties": false, "properties": {"pre_capture": {"default": 5, "description": "Number of seconds before the detection event to include in the recording.", "maximum": 60, "minimum": 0, "title": "Pre-capture seconds", "type": "integer"}, "post_capture": {"default": 5, "description": "Number of seconds after the detection event to include in the recording.", "minimum": 0, "title": "Post-capture seconds", "type": "integer"}, "retain": {"$ref": "#/$defs/ReviewRetainConfig", "description": "Retention settings for recordings of detection events.", "title": "Event retention"}}, "title": "EventsConfig", "type": "object"}, "FaceRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable face recognition for all cameras; can be overridden per-camera.", "title": "Enable face recognition", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size to use for face embeddings (small/large); larger may require GPU.", "title": "Model size"}, "unknown_score": {"default": 0.8, "description": "Distance threshold below which a face is considered a potential match (higher = stricter).", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Unknown score threshold", "type": "number"}, "detection_threshold": {"default": 0.7, "description": "Minimum detection confidence required to consider a face detection valid.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "recognition_threshold": {"default": 0.9, "description": "Face embedding distance threshold to consider two faces a match.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_area": {"default": 750, "description": "Minimum area (pixels) of a detected face box required to attempt recognition.", "title": "Minimum face area", "type": "integer"}, "min_faces": {"default": 1, "description": "Minimum number of face recognitions required before applying a recognized sub-label to a person.", "exclusiveMinimum": 0, "maximum": 6, "title": "Minimum faces", "type": "integer"}, "save_attempts": {"default": 200, "description": "Number of face recognition attempts to retain for recent recognition UI.", "minimum": 0, "title": "Save attempts", "type": "integer"}, "blur_confidence_filter": {"default": true, "description": "Adjust confidence scores based on image blur to reduce false positives for poor quality faces.", "title": "Blur confidence filter", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "FaceRecognitionConfig", "type": "object"}, "FfmpegConfig": {"additionalProperties": false, "properties": {"path": {"default": "default", "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\").", "title": "FFmpeg path", "type": "string"}, "global_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "description": "Global arguments passed to FFmpeg processes.", "title": "FFmpeg global arguments"}, "hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.", "title": "Hardware acceleration arguments"}, "input_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-rtsp-generic", "description": "Input arguments applied to FFmpeg input streams.", "title": "Input arguments"}, "output_args": {"$ref": "#/$defs/FfmpegOutputArgsConfig", "description": "Default output arguments used for different FFmpeg roles such as detect and record.", "title": "Output arguments"}, "retry_interval": {"default": 10.0, "description": "Seconds to wait before attempting to reconnect a camera stream after failure. Default is 10.", "exclusiveMinimum": 0.0, "title": "FFmpeg retry time", "type": "number"}, "apple_compatibility": {"default": false, "description": "Enable HEVC tagging for better Apple player compatibility when recording H.265.", "title": "Apple compatibility", "type": "boolean"}, "gpu": {"default": 0, "description": "Default GPU index used for hardware acceleration if available.", "title": "GPU index", "type": "integer"}}, "title": "FfmpegConfig", "type": "object"}, "FfmpegOutputArgsConfig": {"additionalProperties": false, "properties": {"detect": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "description": "Default output arguments for detect role streams.", "title": "Detect output arguments"}, "record": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "preset-record-generic-audio-aac", "description": "Default output arguments for record role streams.", "title": "Record output arguments"}}, "title": "FfmpegOutputArgsConfig", "type": "object"}, "FilterConfig": {"additionalProperties": false, "properties": {"min_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 0, "description": "Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Minimum object area"}, "max_area": {"anyOf": [{"type": "integer"}, {"type": "number"}], "default": 24000000, "description": "Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).", "title": "Maximum object area"}, "min_ratio": {"default": 0, "description": "Minimum width/height ratio required for the bounding box to qualify.", "title": "Minimum aspect ratio", "type": "number"}, "max_ratio": {"default": 24000000, "description": "Maximum width/height ratio allowed for the bounding box to qualify.", "title": "Maximum aspect ratio", "type": "number"}, "threshold": {"default": 0.7, "description": "Average detection confidence threshold required for the object to be considered a true positive.", "title": "Confidence threshold", "type": "number"}, "min_score": {"default": 0.5, "description": "Minimum single-frame detection confidence required for the object to be counted.", "title": "Minimum confidence", "type": "number"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Polygon coordinates defining where this filter applies within the frame.", "title": "Filter mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "FilterConfig", "type": "object"}, "GenAIConfig": {"additionalProperties": false, "description": "Primary GenAI Config to define GenAI Provider.", "properties": {"api_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "API key required by some providers (can also be set via environment variables).", "title": "API key"}, "base_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Base URL for self-hosted or compatible providers (for example an Ollama instance).", "title": "Base URL"}, "model": {"default": "", "description": "The model to use from the provider for generating descriptions or summaries.", "title": "Model", "type": "string"}, "provider": {"$ref": "#/$defs/GenAIProviderEnum", "description": "The GenAI provider to use (for example: ollama, gemini, openai).", "title": "Provider"}, "roles": {"description": "GenAI roles (chat, descriptions, embeddings); one provider per role.", "items": {"$ref": "#/$defs/GenAIRoleEnum"}, "title": "Roles", "type": "array"}, "provider_options": {"additionalProperties": {}, "default": {}, "description": "Additional provider-specific options to pass to the GenAI client.", "title": "Provider options", "type": "object"}, "runtime_options": {"additionalProperties": {}, "default": {}, "description": "Runtime options passed to the provider for each inference call.", "title": "Runtime options", "type": "object"}}, "required": ["provider"], "title": "GenAIConfig", "type": "object"}, "GenAIObjectConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable GenAI generation of descriptions for tracked objects by default.", "title": "Enable GenAI", "type": "boolean"}, "use_snapshot": {"default": false, "description": "Use object snapshots instead of thumbnails for GenAI description generation.", "title": "Use snapshots", "type": "boolean"}, "prompt": {"default": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "description": "Default prompt template used when generating descriptions with GenAI.", "title": "Caption prompt", "type": "string"}, "object_prompts": {"additionalProperties": {"type": "string"}, "description": "Per-object prompts to customize GenAI outputs for specific labels.", "title": "Object prompts", "type": "object"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object labels to send to GenAI by default.", "title": "GenAI objects"}, "required_zones": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Zones that must be entered for objects to qualify for GenAI description generation.", "title": "Required zones"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails sent to GenAI for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "send_triggers": {"$ref": "#/$defs/GenAIObjectTriggerConfig", "description": "Defines when frames should be sent to GenAI (on end, after updates, etc.).", "title": "GenAI triggers"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether GenAI was enabled in the original static config.", "title": "Original GenAI state"}}, "title": "GenAIObjectConfig", "type": "object"}, "GenAIObjectTriggerConfig": {"additionalProperties": false, "properties": {"tracked_object_end": {"default": true, "description": "Send a request to GenAI when the tracked object ends.", "title": "Send on end", "type": "boolean"}, "after_significant_updates": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Send a request to GenAI after a specified number of significant updates for the tracked object.", "title": "Early GenAI trigger"}}, "title": "GenAIObjectTriggerConfig", "type": "object"}, "GenAIProviderEnum": {"enum": ["openai", "azure_openai", "gemini", "ollama", "llamacpp"], "title": "GenAIProviderEnum", "type": "string"}, "GenAIReviewConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable GenAI-generated descriptions and summaries for review items.", "title": "Enable GenAI descriptions", "type": "boolean"}, "alerts": {"default": true, "description": "Use GenAI to generate descriptions for alert items.", "title": "Enable GenAI for alerts", "type": "boolean"}, "detections": {"default": false, "description": "Use GenAI to generate descriptions for detection items.", "title": "Enable GenAI for detections", "type": "boolean"}, "image_source": {"$ref": "#/$defs/ImageSourceEnum", "default": "preview", "description": "Source of images sent to GenAI ('preview' or 'recordings'); 'recordings' uses higher quality frames but more tokens.", "title": "Review image source"}, "additional_concerns": {"default": [], "description": "A list of additional concerns or notes the GenAI should consider when evaluating activity on this camera.", "items": {"type": "string"}, "title": "Additional concerns", "type": "array"}, "debug_save_thumbnails": {"default": false, "description": "Save thumbnails that are sent to the GenAI provider for debugging and review.", "title": "Save thumbnails", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Tracks whether GenAI review was originally enabled in the static configuration.", "title": "Original GenAI state"}, "preferred_language": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Preferred language to request from the GenAI provider for generated responses.", "title": "Preferred language"}, "activity_context_prompt": {"default": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is.", "description": "Custom prompt describing what is and is not suspicious activity to provide context for GenAI summaries.", "title": "Activity context prompt", "type": "string"}}, "title": "GenAIReviewConfig", "type": "object"}, "GenAIRoleEnum": {"enum": ["chat", "descriptions", "embeddings"], "title": "GenAIRoleEnum", "type": "string"}, "HeaderMappingConfig": {"additionalProperties": false, "properties": {"user": {"default": null, "description": "Header containing the authenticated username provided by the upstream proxy.", "title": "User header", "type": "string"}, "role": {"default": null, "description": "Header containing the authenticated user's role or groups from the upstream proxy.", "title": "Role header", "type": "string"}, "role_map": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "description": "Map upstream group values to Frigate roles (for example map admin groups to the admin role).", "title": "Role mapping"}}, "title": "HeaderMappingConfig", "type": "object"}, "IPv6Config": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable IPv6 support for Frigate services (API and UI) where applicable.", "title": "Enable IPv6", "type": "boolean"}}, "title": "IPv6Config", "type": "object"}, "ImageSourceEnum": {"description": "Image source options for GenAI Review.", "enum": ["preview", "recordings"], "title": "ImageSourceEnum", "type": "string"}, "InputDTypeEnum": {"enum": ["float", "float_denorm", "int"], "title": "InputDTypeEnum", "type": "string"}, "InputTensorEnum": {"enum": ["nchw", "nhwc", "hwnc", "hwcn"], "title": "InputTensorEnum", "type": "string"}, "LicensePlateRecognitionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable license plate recognition for all cameras; can be overridden per-camera.", "title": "Enable LPR", "type": "boolean"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Model size used for text detection/recognition. Most users should use 'small'.", "title": "Model size"}, "detection_threshold": {"default": 0.7, "description": "Detection confidence threshold to begin running OCR on a suspected plate.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Detection threshold", "type": "number"}, "min_area": {"default": 1000, "description": "Minimum plate area (pixels) required to attempt recognition.", "title": "Minimum plate area", "type": "integer"}, "recognition_threshold": {"default": 0.9, "description": "Confidence threshold required for recognized plate text to be attached as a sub-label.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Recognition threshold", "type": "number"}, "min_plate_length": {"default": 4, "description": "Minimum number of characters a recognized plate must contain to be considered valid.", "title": "Min plate length", "type": "integer"}, "format": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional regex to validate recognized plate strings against an expected format.", "title": "Plate format regex"}, "match_distance": {"default": 1, "description": "Number of character mismatches allowed when comparing detected plates to known plates.", "minimum": 0, "title": "Match distance", "type": "integer"}, "known_plates": {"anyOf": [{"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "type": "object"}, {"type": "null"}], "default": {}, "description": "List of plates or regexes to specially track or alert on.", "title": "Known plates"}, "enhancement": {"default": 0, "description": "Enhancement level (0-10) to apply to plate crops prior to OCR; higher values may not always improve results, levels above 5 may only work with night time plates and should be used with caution.", "maximum": 10, "minimum": 0, "title": "Enhancement level", "type": "integer"}, "debug_save_plates": {"default": false, "description": "Save plate crop images for debugging LPR performance.", "title": "Save debug plates", "type": "boolean"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}, "replace_rules": {"description": "Regex replacement rules used to normalize detected plate strings before matching.", "items": {"$ref": "#/$defs/ReplaceRule"}, "title": "Replacement rules", "type": "array"}}, "title": "LicensePlateRecognitionConfig", "type": "object"}, "ListenConfig": {"additionalProperties": false, "properties": {"internal": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 5000, "description": "Internal listening port for Frigate (default 5000).", "title": "Internal port"}, "external": {"anyOf": [{"type": "integer"}, {"type": "string"}], "default": 8971, "description": "External listening port for Frigate (default 8971).", "title": "External port"}}, "title": "ListenConfig", "type": "object"}, "LogLevel": {"enum": ["debug", "info", "warning", "error", "critical"], "title": "LogLevel", "type": "string"}, "LoggerConfig": {"additionalProperties": false, "properties": {"default": {"$ref": "#/$defs/LogLevel", "default": "info", "title": "Logging level", "description": "Default global log verbosity (debug, info, warning, error)."}, "logs": {"additionalProperties": {"$ref": "#/$defs/LogLevel"}, "description": "Per-component log level overrides to increase or decrease verbosity for specific modules.", "title": "Per-process log level", "type": "object"}}, "title": "LoggerConfig", "type": "object"}, "ModelConfig": {"additionalProperties": false, "properties": {"path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a custom detection model file (or plus:// for Frigate+ models).", "title": "Custom object detector model path"}, "labelmap_path": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to a labelmap file that maps numeric classes to string labels for the detector.", "title": "Label map for custom object detector"}, "width": {"default": 320, "description": "Width of the model input tensor in pixels.", "title": "Object detection model input width", "type": "integer"}, "height": {"default": 320, "description": "Height of the model input tensor in pixels.", "title": "Object detection model input height", "type": "integer"}, "labelmap": {"additionalProperties": {"type": "string"}, "description": "Overrides or remapping entries to merge into the standard labelmap.", "title": "Labelmap customization", "type": "object"}, "attributes_map": {"additionalProperties": {"items": {"type": "string"}, "type": "array"}, "default": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "description": "Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).", "title": "Map of object labels to their attribute labels", "type": "object"}, "input_tensor": {"$ref": "#/$defs/InputTensorEnum", "default": "nhwc", "description": "Tensor format expected by the model: 'nhwc' or 'nchw'.", "title": "Model Input Tensor Shape"}, "input_pixel_format": {"$ref": "#/$defs/PixelFormatEnum", "default": "rgb", "description": "Pixel colorspace expected by the model: 'rgb', 'bgr', or 'yuv'.", "title": "Model Input Pixel Color Format"}, "input_dtype": {"$ref": "#/$defs/InputDTypeEnum", "default": "int", "description": "Data type of the model input tensor (for example 'float32').", "title": "Model Input D Type"}, "model_type": {"$ref": "#/$defs/ModelTypeEnum", "default": "ssd", "description": "Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.", "title": "Object Detection Model Type"}}, "title": "ModelConfig", "type": "object"}, "ModelSizeEnum": {"enum": ["small", "large"], "title": "ModelSizeEnum", "type": "string"}, "ModelTypeEnum": {"enum": ["dfine", "rfdetr", "ssd", "yolox", "yolonas", "yolo-generic"], "title": "ModelTypeEnum", "type": "string"}, "MotionConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable motion detection for all cameras; can be overridden per-camera.", "title": "Enable motion detection", "type": "boolean"}, "threshold": {"default": 30, "description": "Pixel difference threshold used by the motion detector; higher values reduce sensitivity (range 1-255).", "maximum": 255, "minimum": 1, "title": "Motion threshold", "type": "integer"}, "lightning_threshold": {"default": 0.8, "description": "Threshold to detect and ignore brief lighting spikes (lower is more sensitive, values between 0.3 and 1.0). This does not prevent motion detection entirely; it merely causes the detector to stop analyzing additional frames once the threshold is exceeded. Motion-based recordings are still created during these events.", "maximum": 1.0, "minimum": 0.3, "title": "Lightning threshold", "type": "number"}, "skip_motion_threshold": {"anyOf": [{"maximum": 1.0, "minimum": 0.0, "type": "number"}, {"type": "null"}], "default": null, "description": "If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto\u2011tracking an object. The trade\u2011off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.", "title": "Skip motion threshold"}, "improve_contrast": {"default": true, "description": "Apply contrast improvement to frames before motion analysis to help detection.", "title": "Improve contrast", "type": "boolean"}, "contour_area": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 10, "description": "Minimum contour area in pixels required for a motion contour to be counted.", "title": "Contour area"}, "delta_alpha": {"default": 0.2, "description": "Alpha blending factor used in frame differencing for motion calculation.", "title": "Delta alpha", "type": "number"}, "frame_alpha": {"default": 0.01, "description": "Alpha value used when blending frames for motion preprocessing.", "title": "Frame alpha", "type": "number"}, "frame_height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": 100, "description": "Height in pixels to scale frames to when computing motion.", "title": "Frame height"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Mask coordinates", "type": "object"}, "mqtt_off_delay": {"default": 30, "description": "Seconds to wait after last motion before publishing an MQTT 'off' state.", "title": "MQTT off delay", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether motion detection was enabled in the original static configuration.", "title": "Original motion state"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/MotionMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}}, "title": "MotionConfig", "type": "object"}, "MotionMaskConfig": {"additionalProperties": false, "description": "Configuration for a single motion mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this motion mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this motion mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of motion mask."}}, "title": "MotionMaskConfig", "type": "object"}, "MqttConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable or disable MQTT integration for state, events, and snapshots.", "title": "Enable MQTT", "type": "boolean"}, "host": {"default": "", "description": "Hostname or IP address of the MQTT broker.", "title": "MQTT host", "type": "string"}, "port": {"default": 1883, "description": "Port of the MQTT broker (usually 1883 for plain MQTT).", "title": "MQTT port", "type": "integer"}, "topic_prefix": {"default": "frigate", "description": "MQTT topic prefix for all Frigate topics; must be unique if running multiple instances.", "title": "Topic prefix", "type": "string"}, "client_id": {"default": "frigate", "description": "Client identifier used when connecting to the MQTT broker; should be unique per instance.", "title": "Client ID", "type": "string"}, "stats_interval": {"default": 60, "description": "Interval in seconds for publishing system and camera stats to MQTT.", "minimum": 15, "title": "Stats interval", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT username; can be provided via environment variables or secrets.", "title": "MQTT username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional MQTT password; can be provided via environment variables or secrets.", "title": "MQTT password"}, "tls_ca_certs": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Path to CA certificate for TLS connections to the broker (for self-signed certs).", "title": "TLS CA certs"}, "tls_client_cert": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Client certificate path for TLS mutual authentication; do not set user/password when using client certs.", "title": "Client cert"}, "tls_client_key": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Private key path for the client certificate.", "title": "Client key"}, "tls_insecure": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Allow insecure TLS connections by skipping hostname verification (not recommended).", "title": "TLS insecure"}, "qos": {"default": 0, "description": "Quality of Service level for MQTT publishes/subscriptions (0, 1, or 2).", "title": "MQTT QoS", "type": "integer"}}, "title": "MqttConfig", "type": "object"}, "NetworkingConfig": {"additionalProperties": false, "properties": {"ipv6": {"$ref": "#/$defs/IPv6Config", "description": "IPv6-specific settings for Frigate network services.", "title": "IPv6 configuration"}, "listen": {"$ref": "#/$defs/ListenConfig", "description": "Configuration for internal and external listening ports. This is for advanced users. For the majority of use cases it's recommended to change the ports section of your Docker compose file.", "title": "Listening ports configuration"}}, "title": "NetworkingConfig", "type": "object"}, "NotificationConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable notifications for all cameras; can be overridden per-camera.", "title": "Enable notifications", "type": "boolean"}, "email": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Email address used for push notifications or required by certain notification providers.", "title": "Notification email"}, "cooldown": {"default": 0, "description": "Cooldown (seconds) between notifications to avoid spamming recipients.", "minimum": 0, "title": "Cooldown period", "type": "integer"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether notifications were enabled in the original static configuration.", "title": "Original notifications state"}}, "title": "NotificationConfig", "type": "object"}, "ObjectClassificationType": {"enum": ["sub_label", "attribute"], "title": "ObjectClassificationType", "type": "string"}, "ObjectConfig": {"additionalProperties": false, "properties": {"track": {"default": ["person"], "description": "List of object labels to track for all cameras; can be overridden per-camera.", "items": {"type": "string"}, "title": "Objects to track", "type": "array"}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters applied to detected objects to reduce false positives (area, ratio, confidence).", "title": "Object filters", "type": "object"}, "mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "description": "Mask polygon used to prevent object detection in specified areas.", "title": "Object mask", "type": "object"}, "raw_mask": {"additionalProperties": {"anyOf": [{"$ref": "#/$defs/ObjectMaskConfig"}, {"type": "null"}]}, "title": "Raw Mask", "type": "object"}, "genai": {"$ref": "#/$defs/GenAIObjectConfig", "description": "GenAI options for describing tracked objects and sending frames for generation.", "title": "GenAI object config"}}, "title": "ObjectConfig", "type": "object"}, "ObjectMaskConfig": {"additionalProperties": false, "description": "Configuration for a single object mask.", "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A friendly name for this object mask used in the Frigate UI", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this object mask", "title": "Enabled", "type": "boolean"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "description": "Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.", "title": "Coordinates"}, "raw_coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "", "title": "Raw Coordinates"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of object mask."}}, "title": "ObjectMaskConfig", "type": "object"}, "OnvifConfig": {"additionalProperties": false, "properties": {"host": {"default": "", "description": "Host (and optional scheme) for the ONVIF service for this camera.", "title": "ONVIF host", "type": "string"}, "port": {"default": 8000, "description": "Port number for the ONVIF service.", "title": "ONVIF port", "type": "integer"}, "user": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Username for ONVIF authentication; some devices require admin user for ONVIF.", "title": "ONVIF username"}, "password": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Password for ONVIF authentication.", "title": "ONVIF password"}, "tls_insecure": {"default": false, "description": "Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).", "title": "Disable TLS verify", "type": "boolean"}, "profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.", "title": "ONVIF profile"}, "autotracking": {"$ref": "#/$defs/PtzAutotrackConfig", "description": "Automatically track moving objects and keep them centered in the frame using PTZ camera movements.", "title": "Autotracking"}, "ignore_time_mismatch": {"default": false, "description": "Ignore time synchronization differences between camera and Frigate server for ONVIF communication.", "title": "Ignore time mismatch", "type": "boolean"}}, "title": "OnvifConfig", "type": "object"}, "PixelFormatEnum": {"enum": ["rgb", "bgr", "yuv"], "title": "PixelFormatEnum", "type": "string"}, "ProfileDefinitionConfig": {"additionalProperties": false, "description": "Defines a named profile with a human-readable display name.\n\nThe dict key is the machine name used internally; friendly_name\nis the label shown in the UI and API responses.", "properties": {"friendly_name": {"description": "Display name for this profile shown in the UI.", "title": "Friendly name", "type": "string"}}, "required": ["friendly_name"], "title": "ProfileDefinitionConfig", "type": "object"}, "ProxyConfig": {"additionalProperties": false, "properties": {"header_map": {"$ref": "#/$defs/HeaderMappingConfig", "description": "Map incoming proxy headers to Frigate user and role fields for proxy-based auth.", "title": "Header mapping"}, "logout_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "URL to redirect users to when logging out via the proxy.", "title": "Logout URL"}, "auth_secret": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.", "title": "Proxy secret"}, "default_role": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": "viewer", "description": "Default role assigned to proxy-authenticated users when no role mapping applies.", "title": "Default role"}, "separator": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": ",", "description": "Character used to split multiple values provided in proxy headers.", "title": "Separator character"}}, "title": "ProxyConfig", "type": "object"}, "PtzAutotrackConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable automatic PTZ camera tracking of detected objects.", "title": "Enable Autotracking", "type": "boolean"}, "calibrate_on_startup": {"default": false, "description": "Measure PTZ motor speeds on startup to improve tracking accuracy. Frigate will update config with movement_weights after calibration.", "title": "Calibrate on start", "type": "boolean"}, "zooming": {"$ref": "#/$defs/ZoomingModeEnum", "default": "disabled", "description": "Control zoom behavior: disabled (pan/tilt only), absolute (most compatible), or relative (concurrent pan/tilt/zoom).", "title": "Zoom mode"}, "zoom_factor": {"default": 0.3, "description": "Control zoom level on tracked objects. Lower values keep more scene in view; higher values zoom in closer but may lose tracking. Values between 0.1 and 0.75.", "maximum": 0.75, "minimum": 0.1, "title": "Zoom factor", "type": "number"}, "track": {"default": ["person"], "description": "List of object types that should trigger autotracking.", "items": {"type": "string"}, "title": "Tracked objects", "type": "array"}, "required_zones": {"description": "Objects must enter one of these zones before autotracking begins.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "return_preset": {"default": "home", "description": "ONVIF preset name configured in camera firmware to return to after tracking ends.", "title": "Return preset", "type": "string"}, "timeout": {"default": 10, "description": "Wait this many seconds after losing tracking before returning camera to preset position.", "title": "Return timeout", "type": "integer"}, "movement_weights": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Calibration values automatically generated by camera calibration. Do not modify manually.", "title": "Movement weights"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Internal field to track whether autotracking was enabled in configuration.", "title": "Original autotrack state"}}, "title": "PtzAutotrackConfig", "type": "object"}, "RecordConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable recording for all cameras; can be overridden per-camera.", "title": "Enable recording", "type": "boolean"}, "expire_interval": {"default": 60, "description": "Minutes between cleanup passes that remove expired recording segments.", "title": "Record cleanup interval", "type": "integer"}, "continuous": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings regardless of tracked objects or motion. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Continuous retention"}, "motion": {"$ref": "#/$defs/RecordRetainConfig", "description": "Number of days to retain recordings triggered by motion regardless of tracked objects. Set to 0 if you only want to retain recordings of alerts and detections.", "title": "Motion retention"}, "detections": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for detection events including pre/post capture durations.", "title": "Detection retention"}, "alerts": {"$ref": "#/$defs/EventsConfig", "description": "Recording retention settings for alert events including pre/post capture durations.", "title": "Alert retention"}, "export": {"$ref": "#/$defs/RecordExportConfig", "description": "Settings used when exporting recordings such as timelapse and hardware acceleration.", "title": "Export config"}, "preview": {"$ref": "#/$defs/RecordPreviewConfig", "description": "Settings controlling the quality of recording previews shown in the UI.", "title": "Preview config"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "description": "Indicates whether recording was enabled in the original static configuration.", "title": "Original recording state"}}, "title": "RecordConfig", "type": "object"}, "RecordExportConfig": {"additionalProperties": false, "properties": {"hwaccel_args": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "default": "auto", "description": "Hardware acceleration args to use for export/transcode operations.", "title": "Export hwaccel args"}, "max_concurrent": {"default": 3, "description": "Maximum number of export jobs to process at the same time.", "minimum": 1, "title": "Maximum concurrent exports", "type": "integer"}}, "title": "RecordExportConfig", "type": "object"}, "RecordPreviewConfig": {"additionalProperties": false, "properties": {"quality": {"$ref": "#/$defs/RecordQualityEnum", "default": "medium", "description": "Preview quality level (very_low, low, medium, high, very_high).", "title": "Preview quality"}}, "title": "RecordPreviewConfig", "type": "object"}, "RecordQualityEnum": {"enum": ["very_low", "low", "medium", "high", "very_high"], "title": "RecordQualityEnum", "type": "string"}, "RecordRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 0, "description": "Days to retain recordings.", "minimum": 0.0, "title": "Retention days", "type": "number"}}, "title": "RecordRetainConfig", "type": "object"}, "ReplaceRule": {"additionalProperties": false, "properties": {"pattern": {"title": "Regex pattern", "type": "string"}, "replacement": {"title": "Replacement string", "type": "string"}}, "required": ["pattern", "replacement"], "title": "ReplaceRule", "type": "object"}, "RestreamConfig": {"additionalProperties": true, "properties": {}, "title": "RestreamConfig", "type": "object"}, "RetainConfig": {"additionalProperties": false, "properties": {"default": {"type": "number", "default": 10, "title": "Default retention", "description": "Default number of days to retain snapshots."}, "objects": {"additionalProperties": {"type": "number"}, "description": "Per-object overrides for snapshot retention days.", "title": "Object retention", "type": "object"}}, "title": "RetainConfig", "type": "object"}, "RetainModeEnum": {"enum": ["all", "motion", "active_objects"], "title": "RetainModeEnum", "type": "string"}, "ReviewConfig": {"additionalProperties": false, "properties": {"alerts": {"$ref": "#/$defs/AlertsConfig", "description": "Settings for which tracked objects generate alerts and how alerts are retained.", "title": "Alerts config"}, "detections": {"$ref": "#/$defs/DetectionsConfig", "description": "Settings for which tracked objects generate detections (non-alert) and how detections are retained.", "title": "Detections config"}, "genai": {"$ref": "#/$defs/GenAIReviewConfig", "description": "Controls use of generative AI for producing descriptions and summaries of review items.", "title": "GenAI config"}}, "title": "ReviewConfig", "type": "object"}, "ReviewRetainConfig": {"additionalProperties": false, "properties": {"days": {"default": 10, "description": "Number of days to retain recordings of detection events.", "minimum": 0.0, "title": "Retention days", "type": "number"}, "mode": {"$ref": "#/$defs/RetainModeEnum", "default": "motion", "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).", "title": "Retention mode"}}, "title": "ReviewRetainConfig", "type": "object"}, "SemanticSearchConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable the semantic search feature.", "title": "Enable semantic search", "type": "boolean"}, "reindex": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": false, "description": "Trigger a full reindex of historical tracked objects into the embeddings database.", "title": "Reindex on startup"}, "model": {"anyOf": [{"$ref": "#/$defs/SemanticSearchModelEnum"}, {"type": "string"}, {"type": "null"}], "default": "jinav1", "description": "The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.", "title": "Semantic search model or GenAI provider name"}, "model_size": {"$ref": "#/$defs/ModelSizeEnum", "default": "small", "description": "Select model size; 'small' runs on CPU and 'large' typically requires GPU.", "title": "Model size"}, "device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information", "title": "Device"}}, "title": "SemanticSearchConfig", "type": "object"}, "SemanticSearchModelEnum": {"enum": ["jinav1", "jinav2"], "title": "SemanticSearchModelEnum", "type": "string"}, "SnapshotsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": false, "description": "Enable or disable saving snapshots for all cameras; can be overridden per-camera.", "title": "Enable snapshots", "type": "boolean"}, "timestamp": {"default": false, "description": "Overlay a timestamp on snapshots from API.", "title": "Timestamp overlay", "type": "boolean"}, "bounding_box": {"default": true, "description": "Draw bounding boxes for tracked objects on snapshots from API.", "title": "Bounding box overlay", "type": "boolean"}, "crop": {"default": false, "description": "Crop snapshots from API to the detected object's bounding box.", "title": "Crop snapshot", "type": "boolean"}, "required_zones": {"description": "Zones an object must enter for a snapshot to be saved.", "items": {"type": "string"}, "title": "Required zones", "type": "array"}, "height": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "description": "Height (pixels) to resize snapshots from API to; leave empty to preserve original size.", "title": "Snapshot height"}, "retain": {"$ref": "#/$defs/RetainConfig", "description": "Retention settings for snapshots including default days and per-object overrides.", "title": "Snapshot retention"}, "quality": {"default": 60, "description": "Encode quality for saved snapshots (0-100).", "maximum": 100, "minimum": 0, "title": "Snapshot quality", "type": "integer"}}, "title": "SnapshotsConfig", "type": "object"}, "StationaryConfig": {"additionalProperties": false, "properties": {"interval": {"anyOf": [{"exclusiveMinimum": 0, "type": "integer"}, {"type": "null"}], "default": null, "description": "How often (in frames) to run a detection check to confirm a stationary object.", "title": "Stationary interval"}, "threshold": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "description": "Number of frames with no position change required to mark an object as stationary.", "title": "Stationary threshold"}, "max_frames": {"$ref": "#/$defs/StationaryMaxFramesConfig", "description": "Limits how long stationary objects are tracked before being discarded.", "title": "Max frames"}, "classifier": {"default": true, "description": "Use a visual classifier to detect truly stationary objects even when bounding boxes jitter.", "title": "Enable visual classifier", "type": "boolean"}}, "title": "StationaryConfig", "type": "object"}, "StationaryMaxFramesConfig": {"additionalProperties": false, "properties": {"default": {"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}], "default": null, "title": "Default max frames", "description": "Default maximum frames to track a stationary object before stopping."}, "objects": {"additionalProperties": {"type": "integer"}, "description": "Per-object overrides for maximum frames to track stationary objects.", "title": "Object max frames", "type": "object"}}, "title": "StationaryMaxFramesConfig", "type": "object"}, "StatsConfig": {"additionalProperties": false, "properties": {"amd_gpu_stats": {"default": true, "description": "Enable collection of AMD GPU statistics if an AMD GPU is present.", "title": "AMD GPU stats", "type": "boolean"}, "intel_gpu_stats": {"default": true, "description": "Enable collection of Intel GPU statistics if an Intel GPU is present.", "title": "Intel GPU stats", "type": "boolean"}, "network_bandwidth": {"default": false, "description": "Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).", "title": "Network bandwidth", "type": "boolean"}, "intel_gpu_device": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.", "title": "Intel GPU device"}}, "title": "StatsConfig", "type": "object"}, "TelemetryConfig": {"additionalProperties": false, "properties": {"network_interfaces": {"default": [], "description": "List of network interface name prefixes to monitor for bandwidth statistics.", "items": {"type": "string"}, "title": "Network interfaces", "type": "array"}, "stats": {"$ref": "#/$defs/StatsConfig", "description": "Options to enable/disable collection of various system and GPU statistics.", "title": "System stats"}, "version_check": {"default": true, "description": "Enable an outbound check to detect if a newer Frigate version is available.", "title": "Version check", "type": "boolean"}}, "title": "TelemetryConfig", "type": "object"}, "TimeFormatEnum": {"enum": ["browser", "12hour", "24hour"], "title": "TimeFormatEnum", "type": "string"}, "TimestampEffectEnum": {"enum": ["solid", "shadow"], "title": "TimestampEffectEnum", "type": "string"}, "TimestampPositionEnum": {"enum": ["tl", "tr", "bl", "br"], "title": "TimestampPositionEnum", "type": "string"}, "TimestampStyleConfig": {"additionalProperties": false, "properties": {"position": {"$ref": "#/$defs/TimestampPositionEnum", "default": "tl", "description": "Position of the timestamp on the image (tl/tr/bl/br).", "title": "Timestamp position"}, "format": {"default": "%m/%d/%Y %H:%M:%S", "description": "Datetime format string used for timestamps (Python datetime format codes).", "title": "Timestamp format", "type": "string"}, "color": {"$ref": "#/$defs/ColorConfig", "description": "RGB color values for the timestamp text (all values 0-255).", "title": "Timestamp color"}, "thickness": {"default": 2, "description": "Line thickness of the timestamp text.", "title": "Timestamp thickness", "type": "integer"}, "effect": {"anyOf": [{"$ref": "#/$defs/TimestampEffectEnum"}, {"type": "null"}], "default": null, "description": "Visual effect for the timestamp text (none, solid, shadow).", "title": "Timestamp effect"}}, "title": "TimestampStyleConfig", "type": "object"}, "TlsConfig": {"additionalProperties": false, "properties": {"enabled": {"default": true, "description": "Enable TLS for Frigate's web UI and API on the configured TLS port.", "title": "Enable TLS", "type": "boolean"}}, "title": "TlsConfig", "type": "object"}, "TriggerAction": {"enum": ["notification", "sub_label", "attribute"], "title": "TriggerAction", "type": "string"}, "TriggerConfig": {"additionalProperties": false, "properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional friendly name displayed in the UI for this trigger.", "title": "Friendly name"}, "enabled": {"default": true, "description": "Enable or disable this semantic search trigger.", "title": "Enable this trigger", "type": "boolean"}, "type": {"$ref": "#/$defs/TriggerType", "default": "description", "description": "Type of trigger: 'thumbnail' (match against image) or 'description' (match against text).", "title": "Trigger type"}, "data": {"description": "Text phrase or thumbnail ID to match against tracked objects.", "title": "Trigger content", "type": "string"}, "threshold": {"default": 0.8, "description": "Minimum similarity score (0-1) required to activate this trigger.", "exclusiveMinimum": 0.0, "maximum": 1.0, "title": "Trigger threshold", "type": "number"}, "actions": {"default": [], "description": "List of actions to execute when trigger matches (notification, sub_label, attribute).", "items": {"$ref": "#/$defs/TriggerAction"}, "title": "Trigger actions", "type": "array"}}, "required": ["data"], "title": "TriggerConfig", "type": "object"}, "TriggerType": {"enum": ["thumbnail", "description"], "title": "TriggerType", "type": "string"}, "UIConfig": {"additionalProperties": false, "properties": {"timezone": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Optional timezone to display across the UI (defaults to browser local time if unset).", "title": "Timezone"}, "time_format": {"$ref": "#/$defs/TimeFormatEnum", "default": "browser", "description": "Time format to use in the UI (browser, 12hour, or 24hour).", "title": "Time format"}, "unit_system": {"$ref": "#/$defs/UnitSystemEnum", "default": "metric", "description": "Unit system for display (metric or imperial) used in the UI and MQTT.", "title": "Unit system"}}, "title": "UIConfig", "type": "object"}, "UnitSystemEnum": {"enum": ["imperial", "metric"], "title": "UnitSystemEnum", "type": "string"}, "ZoneConfig": {"properties": {"friendly_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.", "title": "Zone name"}, "enabled": {"default": true, "description": "Enable or disable this zone. Disabled zones are ignored at runtime.", "title": "Enabled", "type": "boolean"}, "enabled_in_config": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "default": null, "title": "Keep track of original state of zone."}, "filters": {"additionalProperties": {"$ref": "#/$defs/FilterConfig"}, "description": "Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.", "title": "Zone filters", "type": "object"}, "coordinates": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).", "title": "Coordinates"}, "distances": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "description": "Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.", "title": "Real-world distances"}, "inertia": {"default": 3, "description": "Number of consecutive frames an object must be detected in the zone before it is considered present. Helps filter out transient detections.", "exclusiveMinimum": 0, "title": "Inertia frames", "type": "integer"}, "loitering_time": {"default": 0, "description": "Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.", "minimum": 0, "title": "Loitering seconds", "type": "integer"}, "speed_threshold": {"anyOf": [{"minimum": 0.1, "type": "number"}, {"type": "null"}], "default": null, "description": "Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.", "title": "Minimum speed"}, "objects": {"anyOf": [{"type": "string"}, {"items": {"type": "string"}, "type": "array"}], "description": "List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.", "title": "Trigger objects"}}, "required": ["coordinates"], "title": "ZoneConfig", "type": "object"}, "ZoomingModeEnum": {"enum": ["disabled", "absolute", "relative"], "title": "ZoomingModeEnum", "type": "string"}}, "additionalProperties": false, "properties": {"version": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Numeric or string version of the active configuration to help detect migrations or format changes.", "title": "Current config version"}, "safe_mode": {"default": false, "description": "When enabled, start Frigate in safe mode with reduced features for troubleshooting.", "title": "Safe mode", "type": "boolean"}, "environment_vars": {"additionalProperties": {"type": "string"}, "description": "Key/value pairs of environment variables to set for the Frigate process in Home Assistant OS. Non-HAOS users must use Docker environment variable configuration instead.", "title": "Environment variables", "type": "object"}, "logger": {"$ref": "#/$defs/LoggerConfig", "description": "Controls default log verbosity and per-component log level overrides.", "title": "Logging"}, "auth": {"$ref": "#/$defs/AuthConfig", "description": "Authentication and session-related settings including cookie and rate limit options.", "title": "Authentication"}, "database": {"$ref": "#/$defs/DatabaseConfig", "description": "Settings for the SQLite database used by Frigate to store tracked object and recording metadata.", "title": "Database"}, "go2rtc": {"$ref": "#/$defs/RestreamConfig", "description": "Settings for the integrated go2rtc restreaming service used for live stream relaying and translation.", "title": "go2rtc"}, "mqtt": {"$ref": "#/$defs/MqttConfig", "description": "Settings for connecting and publishing telemetry, snapshots, and event details to an MQTT broker.", "title": "MQTT"}, "notifications": {"$ref": "#/$defs/NotificationConfig", "description": "Settings to enable and control notifications for all cameras; can be overridden per-camera.", "title": "Notifications"}, "networking": {"$ref": "#/$defs/NetworkingConfig", "description": "Network-related settings such as IPv6 enablement for Frigate endpoints.", "title": "Networking"}, "proxy": {"$ref": "#/$defs/ProxyConfig", "description": "Settings for integrating Frigate behind a reverse proxy that passes authenticated user headers.", "title": "Proxy"}, "telemetry": {"$ref": "#/$defs/TelemetryConfig", "description": "System telemetry and stats options including GPU and network bandwidth monitoring.", "title": "Telemetry"}, "tls": {"$ref": "#/$defs/TlsConfig", "description": "TLS settings for Frigate's web endpoints (port 8971).", "title": "TLS"}, "ui": {"$ref": "#/$defs/UIConfig", "description": "User interface preferences such as timezone, time/date formatting, and units.", "title": "UI"}, "detectors": {"additionalProperties": {"$ref": "#/$defs/BaseDetectorConfig"}, "default": {"cpu": {"type": "cpu"}}, "description": "Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.", "title": "Detector hardware", "type": "object"}, "model": {"$ref": "#/$defs/ModelConfig", "description": "Settings to configure a custom object detection model and its input shape.", "title": "Detection model"}, "genai": {"additionalProperties": {"$ref": "#/$defs/GenAIConfig"}, "description": "Settings for integrated generative AI providers used to generate object descriptions and review summaries.", "title": "Generative AI configuration", "type": "object"}, "cameras": {"additionalProperties": {"$ref": "#/$defs/CameraConfig"}, "description": "Cameras", "title": "Cameras", "type": "object"}, "audio": {"$ref": "#/$defs/AudioConfig", "description": "Settings for audio-based event detection for all cameras; can be overridden per-camera.", "title": "Audio detection"}, "birdseye": {"$ref": "#/$defs/BirdseyeConfig", "description": "Settings for the Birdseye composite view that composes multiple camera feeds into a single layout.", "title": "Birdseye"}, "detect": {"$ref": "#/$defs/DetectConfig", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "title": "Object Detection"}, "ffmpeg": {"$ref": "#/$defs/FfmpegConfig", "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", "title": "FFmpeg"}, "live": {"$ref": "#/$defs/CameraLiveConfig", "description": "Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.", "title": "Live playback"}, "motion": {"anyOf": [{"$ref": "#/$defs/MotionConfig"}, {"type": "null"}], "default": null, "description": "Default motion detection settings applied to cameras unless overridden per-camera.", "title": "Motion detection"}, "objects": {"$ref": "#/$defs/ObjectConfig", "description": "Object tracking defaults including which labels to track and per-object filters.", "title": "Objects"}, "record": {"$ref": "#/$defs/RecordConfig", "description": "Recording and retention settings applied to cameras unless overridden per-camera.", "title": "Recording"}, "review": {"$ref": "#/$defs/ReviewConfig", "description": "Settings that control alerts, detections, and GenAI review summaries used by the UI and storage.", "title": "Review"}, "snapshots": {"$ref": "#/$defs/SnapshotsConfig", "description": "Settings for API-generated snapshots of tracked objects for all cameras; can be overridden per-camera.", "title": "Snapshots"}, "timestamp_style": {"$ref": "#/$defs/TimestampStyleConfig", "description": "Styling options for in-feed timestamps applied to debug view and snapshots.", "title": "Timestamp style"}, "audio_transcription": {"$ref": "#/$defs/AudioTranscriptionConfig", "description": "Settings for live and speech audio transcription used for events and live captions.", "title": "Audio transcription"}, "classification": {"$ref": "#/$defs/ClassificationConfig", "description": "Settings for classification models used to refine object labels or state classification.", "title": "Object classification"}, "semantic_search": {"$ref": "#/$defs/SemanticSearchConfig", "description": "Settings for Semantic Search which builds and queries object embeddings to find similar items.", "title": "Semantic Search"}, "face_recognition": {"$ref": "#/$defs/FaceRecognitionConfig", "description": "Settings for face detection and recognition for all cameras; can be overridden per-camera.", "title": "Face recognition"}, "lpr": {"$ref": "#/$defs/LicensePlateRecognitionConfig", "description": "License plate recognition settings including detection thresholds, formatting, and known plates.", "title": "License Plate Recognition"}, "camera_groups": {"additionalProperties": {"$ref": "#/$defs/CameraGroupConfig"}, "description": "Configuration for named camera groups used to organize cameras in the UI.", "title": "Camera groups", "type": "object"}, "profiles": {"additionalProperties": {"$ref": "#/$defs/ProfileDefinitionConfig"}, "description": "Named profile definitions with friendly names. Camera profiles must reference names defined here.", "title": "Profiles", "type": "object"}, "active_profile": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "description": "Currently active profile name. Runtime-only, not persisted in YAML.", "title": "Active profile"}}, "required": ["mqtt", "cameras"], "title": "FrigateConfig", "type": "object"} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/config-snapshot.json b/web/e2e/fixtures/mock-data/config-snapshot.json index 6b87982c48..2df402bc99 100644 --- a/web/e2e/fixtures/mock-data/config-snapshot.json +++ b/web/e2e/fixtures/mock-data/config-snapshot.json @@ -1 +1 @@ -{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "date_style": "short", "time_style": "medium", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi"}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "scream", "speech", "yell"], "filters": {"a_capella": {"threshold": 0.8}, "accelerating": {"threshold": 0.8}, "accordion": {"threshold": 0.8}, "acoustic_guitar": {"threshold": 0.8}, "afrobeat": {"threshold": 0.8}, "air_brake": {"threshold": 0.8}, "air_conditioning": {"threshold": 0.8}, "air_horn": {"threshold": 0.8}, "aircraft": {"threshold": 0.8}, "aircraft_engine": {"threshold": 0.8}, "alarm": {"threshold": 0.8}, "alarm_clock": {"threshold": 0.8}, "ambient_music": {"threshold": 0.8}, "ambulance": {"threshold": 0.8}, "angry_music": {"threshold": 0.8}, "animal": {"threshold": 0.8}, "applause": {"threshold": 0.8}, "arrow": {"threshold": 0.8}, "artillery_fire": {"threshold": 0.8}, "babbling": {"threshold": 0.8}, "background_music": {"threshold": 0.8}, "bagpipes": {"threshold": 0.8}, "bang": {"threshold": 0.8}, "banjo": {"threshold": 0.8}, "bark": {"threshold": 0.8}, "basketball_bounce": {"threshold": 0.8}, "bass_drum": {"threshold": 0.8}, "bass_guitar": {"threshold": 0.8}, "bathtub": {"threshold": 0.8}, "beatboxing": {"threshold": 0.8}, "beep": {"threshold": 0.8}, "bell": {"threshold": 0.8}, "bellow": {"threshold": 0.8}, "bicycle": {"threshold": 0.8}, "bicycle_bell": {"threshold": 0.8}, "bird": {"threshold": 0.8}, "biting": {"threshold": 0.8}, "bleat": {"threshold": 0.8}, "blender": {"threshold": 0.8}, "bluegrass": {"threshold": 0.8}, "blues": {"threshold": 0.8}, "boat": {"threshold": 0.8}, "boiling": {"threshold": 0.8}, "boing": {"threshold": 0.8}, "boom": {"threshold": 0.8}, "bouncing": {"threshold": 0.8}, "bow-wow": {"threshold": 0.8}, "bowed_string_instrument": {"threshold": 0.8}, "brass_instrument": {"threshold": 0.8}, "breaking": {"threshold": 0.8}, "breathing": {"threshold": 0.8}, "burping": {"threshold": 0.8}, "burst": {"threshold": 0.8}, "bus": {"threshold": 0.8}, "busy_signal": {"threshold": 0.8}, "buzz": {"threshold": 0.8}, "buzzer": {"threshold": 0.8}, "cacophony": {"threshold": 0.8}, "camera": {"threshold": 0.8}, "cap_gun": {"threshold": 0.8}, "car": {"threshold": 0.8}, "car_alarm": {"threshold": 0.8}, "car_passing_by": {"threshold": 0.8}, "carnatic_music": {"threshold": 0.8}, "cash_register": {"threshold": 0.8}, "cat": {"threshold": 0.8}, "caterwaul": {"threshold": 0.8}, "cattle": {"threshold": 0.8}, "caw": {"threshold": 0.8}, "cello": {"threshold": 0.8}, "chainsaw": {"threshold": 0.8}, "change_ringing": {"threshold": 0.8}, "chant": {"threshold": 0.8}, "chatter": {"threshold": 0.8}, "cheering": {"threshold": 0.8}, "chewing": {"threshold": 0.8}, "chicken": {"threshold": 0.8}, "child_singing": {"threshold": 0.8}, "children_playing": {"threshold": 0.8}, "chime": {"threshold": 0.8}, "chink": {"threshold": 0.8}, "chird": {"threshold": 0.8}, "chirp": {"threshold": 0.8}, "chirp_tone": {"threshold": 0.8}, "choir": {"threshold": 0.8}, "chop": {"threshold": 0.8}, "chopping": {"threshold": 0.8}, "chorus_effect": {"threshold": 0.8}, "christian_music": {"threshold": 0.8}, "christmas_music": {"threshold": 0.8}, "church_bell": {"threshold": 0.8}, "civil_defense_siren": {"threshold": 0.8}, "clang": {"threshold": 0.8}, "clapping": {"threshold": 0.8}, "clarinet": {"threshold": 0.8}, "classical_music": {"threshold": 0.8}, "clatter": {"threshold": 0.8}, "clickety-clack": {"threshold": 0.8}, "clicking": {"threshold": 0.8}, "clip-clop": {"threshold": 0.8}, "clock": {"threshold": 0.8}, "cluck": {"threshold": 0.8}, "cock-a-doodle-doo": {"threshold": 0.8}, "coin": {"threshold": 0.8}, "computer_keyboard": {"threshold": 0.8}, "coo": {"threshold": 0.8}, "cough": {"threshold": 0.8}, "country": {"threshold": 0.8}, "cowbell": {"threshold": 0.8}, "crack": {"threshold": 0.8}, "crackle": {"threshold": 0.8}, "creak": {"threshold": 0.8}, "cricket": {"threshold": 0.8}, "croak": {"threshold": 0.8}, "crow": {"threshold": 0.8}, "crowd": {"threshold": 0.8}, "crumpling": {"threshold": 0.8}, "crunch": {"threshold": 0.8}, "crushing": {"threshold": 0.8}, "crying": {"threshold": 0.8}, "cupboard_open_or_close": {"threshold": 0.8}, "cutlery": {"threshold": 0.8}, "cymbal": {"threshold": 0.8}, "dance_music": {"threshold": 0.8}, "dental_drill's_drill": {"threshold": 0.8}, "dial_tone": {"threshold": 0.8}, "didgeridoo": {"threshold": 0.8}, "ding": {"threshold": 0.8}, "ding-dong": {"threshold": 0.8}, "disco": {"threshold": 0.8}, "dishes": {"threshold": 0.8}, "distortion": {"threshold": 0.8}, "dog": {"threshold": 0.8}, "dogs": {"threshold": 0.8}, "door": {"threshold": 0.8}, "doorbell": {"threshold": 0.8}, "double_bass": {"threshold": 0.8}, "drawer_open_or_close": {"threshold": 0.8}, "drill": {"threshold": 0.8}, "drip": {"threshold": 0.8}, "drum": {"threshold": 0.8}, "drum_and_bass": {"threshold": 0.8}, "drum_kit": {"threshold": 0.8}, "drum_machine": {"threshold": 0.8}, "drum_roll": {"threshold": 0.8}, "dubstep": {"threshold": 0.8}, "duck": {"threshold": 0.8}, "echo": {"threshold": 0.8}, "effects_unit": {"threshold": 0.8}, "electric_guitar": {"threshold": 0.8}, "electric_piano": {"threshold": 0.8}, "electric_shaver": {"threshold": 0.8}, "electric_toothbrush": {"threshold": 0.8}, "electronic_dance_music": {"threshold": 0.8}, "electronic_music": {"threshold": 0.8}, "electronic_organ": {"threshold": 0.8}, "electronic_tuner": {"threshold": 0.8}, "electronica": {"threshold": 0.8}, "emergency_vehicle": {"threshold": 0.8}, "engine": {"threshold": 0.8}, "engine_knocking": {"threshold": 0.8}, "engine_starting": {"threshold": 0.8}, "environmental_noise": {"threshold": 0.8}, "eruption": {"threshold": 0.8}, "exciting_music": {"threshold": 0.8}, "explosion": {"threshold": 0.8}, "fart": {"threshold": 0.8}, "field_recording": {"threshold": 0.8}, "filing": {"threshold": 0.8}, "fill": {"threshold": 0.8}, "finger_snapping": {"threshold": 0.8}, "fire": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "fire_engine": {"threshold": 0.8}, "firecracker": {"threshold": 0.8}, "fireworks": {"threshold": 0.8}, "fixed-wing_aircraft": {"threshold": 0.8}, "flamenco": {"threshold": 0.8}, "flap": {"threshold": 0.8}, "flapping_wings": {"threshold": 0.8}, "flute": {"threshold": 0.8}, "fly": {"threshold": 0.8}, "foghorn": {"threshold": 0.8}, "folk_music": {"threshold": 0.8}, "footsteps": {"threshold": 0.8}, "fowl": {"threshold": 0.8}, "french_horn": {"threshold": 0.8}, "frog": {"threshold": 0.8}, "frying": {"threshold": 0.8}, "funk": {"threshold": 0.8}, "fusillade": {"threshold": 0.8}, "gargling": {"threshold": 0.8}, "gasp": {"threshold": 0.8}, "gears": {"threshold": 0.8}, "glass": {"threshold": 0.8}, "glockenspiel": {"threshold": 0.8}, "goat": {"threshold": 0.8}, "gobble": {"threshold": 0.8}, "gong": {"threshold": 0.8}, "goose": {"threshold": 0.8}, "gospel_music": {"threshold": 0.8}, "groan": {"threshold": 0.8}, "growling": {"threshold": 0.8}, "grunge": {"threshold": 0.8}, "grunt": {"threshold": 0.8}, "guitar": {"threshold": 0.8}, "gunshot": {"threshold": 0.8}, "gurgling": {"threshold": 0.8}, "gush": {"threshold": 0.8}, "hair_dryer": {"threshold": 0.8}, "hammer": {"threshold": 0.8}, "hammond_organ": {"threshold": 0.8}, "hands": {"threshold": 0.8}, "happy_music": {"threshold": 0.8}, "harmonic": {"threshold": 0.8}, "harmonica": {"threshold": 0.8}, "harp": {"threshold": 0.8}, "harpsichord": {"threshold": 0.8}, "heart_murmur": {"threshold": 0.8}, "heartbeat": {"threshold": 0.8}, "heavy_engine": {"threshold": 0.8}, "heavy_metal": {"threshold": 0.8}, "helicopter": {"threshold": 0.8}, "hi-hat": {"threshold": 0.8}, "hiccup": {"threshold": 0.8}, "hip_hop_music": {"threshold": 0.8}, "hiss": {"threshold": 0.8}, "honk": {"threshold": 0.8}, "hoot": {"threshold": 0.8}, "horse": {"threshold": 0.8}, "house_music": {"threshold": 0.8}, "howl": {"threshold": 0.8}, "hum": {"threshold": 0.8}, "humming": {"threshold": 0.8}, "ice_cream_truck": {"threshold": 0.8}, "idling": {"threshold": 0.8}, "independent_music": {"threshold": 0.8}, "insect": {"threshold": 0.8}, "inside": {"threshold": 0.8}, "jackhammer": {"threshold": 0.8}, "jazz": {"threshold": 0.8}, "jet_engine": {"threshold": 0.8}, "jingle": {"threshold": 0.8}, "jingle_bell": {"threshold": 0.8}, "keyboard": {"threshold": 0.8}, "keys_jangling": {"threshold": 0.8}, "knock": {"threshold": 0.8}, "laughter": {"threshold": 0.8}, "lawn_mower": {"threshold": 0.8}, "light_engine": {"threshold": 0.8}, "liquid": {"threshold": 0.8}, "livestock": {"threshold": 0.8}, "lullaby": {"threshold": 0.8}, "machine_gun": {"threshold": 0.8}, "mains_hum": {"threshold": 0.8}, "mallet_percussion": {"threshold": 0.8}, "mandolin": {"threshold": 0.8}, "mantra": {"threshold": 0.8}, "maraca": {"threshold": 0.8}, "marimba": {"threshold": 0.8}, "mechanical_fan": {"threshold": 0.8}, "mechanisms": {"threshold": 0.8}, "medium_engine": {"threshold": 0.8}, "meow": {"threshold": 0.8}, "microwave_oven": {"threshold": 0.8}, "middle_eastern_music": {"threshold": 0.8}, "moo": {"threshold": 0.8}, "mosquito": {"threshold": 0.8}, "motor_vehicle": {"threshold": 0.8}, "motorboat": {"threshold": 0.8}, "motorcycle": {"threshold": 0.8}, "mouse": {"threshold": 0.8}, "music": {"threshold": 0.8}, "music_for_children": {"threshold": 0.8}, "music_of_africa": {"threshold": 0.8}, "music_of_asia": {"threshold": 0.8}, "music_of_bollywood": {"threshold": 0.8}, "music_of_latin_america": {"threshold": 0.8}, "musical_instrument": {"threshold": 0.8}, "neigh": {"threshold": 0.8}, "new-age_music": {"threshold": 0.8}, "noise": {"threshold": 0.8}, "ocean": {"threshold": 0.8}, "oink": {"threshold": 0.8}, "opera": {"threshold": 0.8}, "orchestra": {"threshold": 0.8}, "organ": {"threshold": 0.8}, "outside": {"threshold": 0.8}, "owl": {"threshold": 0.8}, "pant": {"threshold": 0.8}, "patter": {"threshold": 0.8}, "percussion": {"threshold": 0.8}, "pets": {"threshold": 0.8}, "piano": {"threshold": 0.8}, "pig": {"threshold": 0.8}, "pigeon": {"threshold": 0.8}, "ping": {"threshold": 0.8}, "pink_noise": {"threshold": 0.8}, "pizzicato": {"threshold": 0.8}, "plop": {"threshold": 0.8}, "plucked_string_instrument": {"threshold": 0.8}, "police_car": {"threshold": 0.8}, "pop_music": {"threshold": 0.8}, "pour": {"threshold": 0.8}, "power_tool": {"threshold": 0.8}, "power_windows": {"threshold": 0.8}, "printer": {"threshold": 0.8}, "progressive_rock": {"threshold": 0.8}, "propeller": {"threshold": 0.8}, "psychedelic_rock": {"threshold": 0.8}, "pulleys": {"threshold": 0.8}, "pulse": {"threshold": 0.8}, "pump": {"threshold": 0.8}, "punk_rock": {"threshold": 0.8}, "purr": {"threshold": 0.8}, "quack": {"threshold": 0.8}, "race_car": {"threshold": 0.8}, "radio": {"threshold": 0.8}, "rail_transport": {"threshold": 0.8}, "railroad_car": {"threshold": 0.8}, "rain": {"threshold": 0.8}, "rain_on_surface": {"threshold": 0.8}, "raindrop": {"threshold": 0.8}, "rapping": {"threshold": 0.8}, "ratchet": {"threshold": 0.8}, "rats": {"threshold": 0.8}, "rattle": {"threshold": 0.8}, "reggae": {"threshold": 0.8}, "reverberation": {"threshold": 0.8}, "reversing_beeps": {"threshold": 0.8}, "rhythm_and_blues": {"threshold": 0.8}, "rimshot": {"threshold": 0.8}, "ringtone": {"threshold": 0.8}, "roar": {"threshold": 0.8}, "roaring_cats": {"threshold": 0.8}, "rock_and_roll": {"threshold": 0.8}, "rock_music": {"threshold": 0.8}, "roll": {"threshold": 0.8}, "rowboat": {"threshold": 0.8}, "rub": {"threshold": 0.8}, "rumble": {"threshold": 0.8}, "run": {"threshold": 0.8}, "rustle": {"threshold": 0.8}, "rustling_leaves": {"threshold": 0.8}, "sad_music": {"threshold": 0.8}, "sailboat": {"threshold": 0.8}, "salsa_music": {"threshold": 0.8}, "sampler": {"threshold": 0.8}, "sanding": {"threshold": 0.8}, "sawing": {"threshold": 0.8}, "saxophone": {"threshold": 0.8}, "scary_music": {"threshold": 0.8}, "scissors": {"threshold": 0.8}, "scrape": {"threshold": 0.8}, "scratch": {"threshold": 0.8}, "scratching": {"threshold": 0.8}, "sewing_machine": {"threshold": 0.8}, "shatter": {"threshold": 0.8}, "sheep": {"threshold": 0.8}, "ship": {"threshold": 0.8}, "shofar": {"threshold": 0.8}, "shuffle": {"threshold": 0.8}, "shuffling_cards": {"threshold": 0.8}, "sidetone": {"threshold": 0.8}, "sigh": {"threshold": 0.8}, "silence": {"threshold": 0.8}, "sine_wave": {"threshold": 0.8}, "singing": {"threshold": 0.8}, "singing_bowl": {"threshold": 0.8}, "single-lens_reflex_camera": {"threshold": 0.8}, "sink": {"threshold": 0.8}, "siren": {"threshold": 0.8}, "sitar": {"threshold": 0.8}, "sizzle": {"threshold": 0.8}, "ska": {"threshold": 0.8}, "skateboard": {"threshold": 0.8}, "skidding": {"threshold": 0.8}, "slam": {"threshold": 0.8}, "slap": {"threshold": 0.8}, "sliding_door": {"threshold": 0.8}, "slosh": {"threshold": 0.8}, "smash": {"threshold": 0.8}, "smoke_detector": {"threshold": 0.8}, "snake": {"threshold": 0.8}, "snare_drum": {"threshold": 0.8}, "sneeze": {"threshold": 0.8}, "snicker": {"threshold": 0.8}, "sniff": {"threshold": 0.8}, "snoring": {"threshold": 0.8}, "snort": {"threshold": 0.8}, "sodeling": {"threshold": 0.8}, "sonar": {"threshold": 0.8}, "song": {"threshold": 0.8}, "soul_music": {"threshold": 0.8}, "sound_effect": {"threshold": 0.8}, "soundtrack_music": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "splash": {"threshold": 0.8}, "splinter": {"threshold": 0.8}, "spray": {"threshold": 0.8}, "squawk": {"threshold": 0.8}, "squeak": {"threshold": 0.8}, "squeal": {"threshold": 0.8}, "squish": {"threshold": 0.8}, "static": {"threshold": 0.8}, "steam": {"threshold": 0.8}, "steam_whistle": {"threshold": 0.8}, "steel_guitar": {"threshold": 0.8}, "steelpan": {"threshold": 0.8}, "stir": {"threshold": 0.8}, "stomach_rumble": {"threshold": 0.8}, "stream": {"threshold": 0.8}, "string_section": {"threshold": 0.8}, "strum": {"threshold": 0.8}, "subway": {"threshold": 0.8}, "swing_music": {"threshold": 0.8}, "synthesizer": {"threshold": 0.8}, "synthetic_singing": {"threshold": 0.8}, "tabla": {"threshold": 0.8}, "tambourine": {"threshold": 0.8}, "tap": {"threshold": 0.8}, "tapping": {"threshold": 0.8}, "tearing": {"threshold": 0.8}, "techno": {"threshold": 0.8}, "telephone": {"threshold": 0.8}, "telephone_bell_ringing": {"threshold": 0.8}, "telephone_dialing": {"threshold": 0.8}, "television": {"threshold": 0.8}, "tender_music": {"threshold": 0.8}, "theme_music": {"threshold": 0.8}, "theremin": {"threshold": 0.8}, "throat_clearing": {"threshold": 0.8}, "throbbing": {"threshold": 0.8}, "thump": {"threshold": 0.8}, "thunder": {"threshold": 0.8}, "thunderstorm": {"threshold": 0.8}, "thunk": {"threshold": 0.8}, "tick": {"threshold": 0.8}, "tick-tock": {"threshold": 0.8}, "timpani": {"threshold": 0.8}, "tire_squeal": {"threshold": 0.8}, "toilet_flush": {"threshold": 0.8}, "tools": {"threshold": 0.8}, "toot": {"threshold": 0.8}, "toothbrush": {"threshold": 0.8}, "traditional_music": {"threshold": 0.8}, "traffic_noise": {"threshold": 0.8}, "train": {"threshold": 0.8}, "train_horn": {"threshold": 0.8}, "train_wheels_squealing": {"threshold": 0.8}, "train_whistle": {"threshold": 0.8}, "trance_music": {"threshold": 0.8}, "trickle": {"threshold": 0.8}, "trombone": {"threshold": 0.8}, "truck": {"threshold": 0.8}, "trumpet": {"threshold": 0.8}, "tubular_bells": {"threshold": 0.8}, "tuning_fork": {"threshold": 0.8}, "turkey": {"threshold": 0.8}, "typewriter": {"threshold": 0.8}, "typing": {"threshold": 0.8}, "ukulele": {"threshold": 0.8}, "vacuum_cleaner": {"threshold": 0.8}, "vehicle": {"threshold": 0.8}, "vibraphone": {"threshold": 0.8}, "vibration": {"threshold": 0.8}, "video_game_music": {"threshold": 0.8}, "violin": {"threshold": 0.8}, "vocal_music": {"threshold": 0.8}, "water": {"threshold": 0.8}, "water_tap": {"threshold": 0.8}, "waterfall": {"threshold": 0.8}, "waves": {"threshold": 0.8}, "wedding_music": {"threshold": 0.8}, "whack": {"threshold": 0.8}, "whale_vocalization": {"threshold": 0.8}, "wheeze": {"threshold": 0.8}, "whimper_dog": {"threshold": 0.8}, "whip": {"threshold": 0.8}, "whir": {"threshold": 0.8}, "whispering": {"threshold": 0.8}, "whistle": {"threshold": 0.8}, "whistling": {"threshold": 0.8}, "white_noise": {"threshold": 0.8}, "whoop": {"threshold": 0.8}, "whoosh": {"threshold": 0.8}, "wild_animals": {"threshold": 0.8}, "wind": {"threshold": 0.8}, "wind_chime": {"threshold": 0.8}, "wind_instrument": {"threshold": 0.8}, "wind_noise": {"threshold": 0.8}, "wood": {"threshold": 0.8}, "wood_block": {"threshold": 0.8}, "writing": {"threshold": 0.8}, "yell": {"threshold": 0.8}, "yip": {"threshold": 0.8}, "zing": {"threshold": 0.8}, "zipper": {"threshold": 0.8}, "zither": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "auto"}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}} \ No newline at end of file +{"version": null, "safe_mode": false, "environment_vars": {}, "logger": {"default": "info", "logs": {}}, "auth": {"enabled": true, "reset_admin_password": false, "cookie_name": "frigate_token", "cookie_secure": false, "session_length": 86400, "refresh_time": 1800, "failed_login_rate_limit": null, "trusted_proxies": [], "hash_iterations": 600000, "roles": {"admin": [], "viewer": []}, "admin_first_time_login": false}, "database": {"path": "/config/frigate.db"}, "go2rtc": {}, "mqtt": {"enabled": true, "host": "mqtt", "port": 1883, "topic_prefix": "frigate", "client_id": "frigate", "stats_interval": 60, "user": null, "password": null, "tls_ca_certs": null, "tls_client_cert": null, "tls_client_key": null, "tls_insecure": null, "qos": 0}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "networking": {"ipv6": {"enabled": false}, "listen": {"internal": 5000, "external": 8971}}, "proxy": {"header_map": {"user": null, "role": null, "role_map": {}}, "logout_url": null, "auth_secret": null, "default_role": "viewer", "separator": ","}, "telemetry": {"network_interfaces": [], "stats": {"amd_gpu_stats": true, "intel_gpu_stats": true, "network_bandwidth": false, "intel_gpu_device": null}, "version_check": true}, "tls": {"enabled": true}, "ui": {"timezone": null, "time_format": "browser", "unit_system": "metric"}, "detectors": {"cpu": {"type": "cpu", "model": {"path": "/cpu_model.tflite", "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd"}, "model_path": null}}, "model": {"path": null, "labelmap_path": null, "width": 320, "height": 320, "labelmap": {}, "attributes_map": {"person": ["amazon", "face"], "car": ["amazon", "an_post", "canada_post", "dhl", "dpd", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "motorcycle": ["license_plate"]}, "input_tensor": "nhwc", "input_pixel_format": "rgb", "input_dtype": "int", "model_type": "ssd", "all_attributes": ["amazon", "an_post", "canada_post", "dhl", "dpd", "face", "fedex", "gls", "license_plate", "nzpost", "postnl", "postnord", "purolator", "royal_mail", "ups", "usps"], "colormap": {}}, "genai": {}, "cameras": {"front_door": {"name": "front_door", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"front_door": "front_door"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "backyard": {"name": "backyard", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"backyard": "backyard"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}, "garage": {"name": "garage", "friendly_name": null, "enabled": true, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": false, "num_threads": 2}, "audio_transcription": {"enabled": false, "enabled_in_config": false, "live_enabled": false}, "birdseye": {"enabled": true, "mode": "objects", "order": 0}, "detect": {"enabled": false, "height": 720, "width": 1280, "fps": 5, "min_initialized": 2, "max_disappeared": 25, "stationary": {"interval": 50, "threshold": 50, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "face_recognition": {"enabled": false, "min_area": 750}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0, "inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["record", "detect"], "global_args": [], "hwaccel_args": [], "input_args": []}]}, "live": {"streams": {"garage": "garage"}, "height": 720, "quality": 8}, "lpr": {"enabled": false, "expire_time": 3, "min_area": 1000, "enhancement": 0}, "motion": {"enabled": true, "threshold": 30, "lightning_threshold": 0.8, "skip_motion_threshold": null, "improve_contrast": true, "contour_area": 10, "delta_alpha": 0.2, "frame_alpha": 0.01, "frame_height": 100, "mask": {}, "mqtt_off_delay": 30, "enabled_in_config": null}, "objects": {"track": ["person"], "filters": {"person": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.5, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": false}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": false}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": true, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": true}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": false, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "semantic_search": {"triggers": {}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "best_image_timeout": 60, "mqtt": {"enabled": true, "timestamp": true, "bounding_box": true, "crop": true, "height": 270, "required_zones": [], "quality": 70}, "notifications": {"enabled": false, "email": null, "cooldown": 0, "enabled_in_config": false}, "onvif": {"host": "", "port": 8000, "user": null, "password": null, "tls_insecure": false, "profile": null, "autotracking": {"enabled": false, "calibrate_on_startup": false, "zooming": "disabled", "zoom_factor": 0.3, "track": ["person"], "required_zones": [], "return_preset": "home", "timeout": 10, "movement_weights": [], "enabled_in_config": false}, "ignore_time_mismatch": false}, "type": "generic", "ui": {"order": 0, "dashboard": true, "review": true}, "webui_url": null, "profiles": {}, "zones": {}, "enabled_in_config": true}}, "audio": {"enabled": false, "max_not_heard": 30, "min_volume": 500, "listen": ["bark", "fire_alarm", "speech", "yell"], "filters": {"bark": {"threshold": 0.8}, "fire_alarm": {"threshold": 0.8}, "speech": {"threshold": 0.8}, "yell": {"threshold": 0.8}}, "enabled_in_config": null, "num_threads": 2}, "birdseye": {"enabled": true, "mode": "objects", "restream": false, "width": 1280, "height": 720, "quality": 8, "inactivity_threshold": 30, "layout": {"scaling_factor": 2.0, "max_cameras": null}, "idle_heartbeat_fps": 0.0}, "detect": {"enabled": false, "height": null, "width": null, "fps": 5, "min_initialized": null, "max_disappeared": null, "stationary": {"interval": null, "threshold": null, "max_frames": {"default": null, "objects": {}}, "classifier": true}, "annotation_offset": 0}, "ffmpeg": {"path": "default", "global_args": ["-hide_banner", "-loglevel", "warning", "-threads", "2"], "hwaccel_args": "preset-vaapi", "input_args": "preset-rtsp-generic", "output_args": {"detect": ["-threads", "2", "-f", "rawvideo", "-pix_fmt", "yuv420p"], "record": "preset-record-generic-audio-aac"}, "retry_interval": 10.0, "apple_compatibility": false, "gpu": 0}, "live": {"streams": [], "height": 720, "quality": 8}, "motion": null, "objects": {"track": ["person"], "filters": {"royal_mail": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "an_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "ups": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnord": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dhl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "postnl": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "usps": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "face": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "license_plate": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "dpd": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "amazon": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "fedex": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "canada_post": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "nzpost": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "gls": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}, "purolator": {"min_area": 0, "max_area": 24000000, "min_ratio": 0, "max_ratio": 24000000, "threshold": 0.7, "min_score": 0.7, "mask": {}}}, "mask": {}, "genai": {"enabled": false, "use_snapshot": false, "prompt": "Analyze the sequence of images containing the {label}. Focus on the likely intent or behavior of the {label} based on its actions and movement, rather than describing its appearance or the surroundings. Consider what the {label} is doing, why, and what it might do next.", "object_prompts": {}, "objects": [], "required_zones": [], "debug_save_thumbnails": false, "send_triggers": {"tracked_object_end": true, "after_significant_updates": null}, "enabled_in_config": null}}, "record": {"enabled": false, "expire_interval": 60, "continuous": {"days": 0}, "motion": {"days": 0}, "detections": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "alerts": {"pre_capture": 5, "post_capture": 5, "retain": {"days": 10, "mode": "motion"}}, "export": {"hwaccel_args": "preset-vaapi", "max_concurrent": 3}, "preview": {"quality": "medium"}, "enabled_in_config": null}, "review": {"alerts": {"enabled": true, "labels": ["person", "car"], "required_zones": [], "enabled_in_config": null, "cutoff_time": 40}, "detections": {"enabled": true, "labels": null, "required_zones": [], "cutoff_time": 30, "enabled_in_config": null}, "genai": {"enabled": false, "alerts": true, "detections": false, "image_source": "preview", "additional_concerns": [], "debug_save_thumbnails": false, "enabled_in_config": null, "preferred_language": null, "activity_context_prompt": "### Normal Activity Indicators (Level 0)\n- Known/verified people in any zone at any time\n- People with pets in residential areas\n- Routine residential vehicle access during daytime/evening (6 AM - 10 PM): entering, exiting, loading/unloading items \u2014 normal commute and travel patterns\n- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving\n- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime\n- Activity confined to public areas only (sidewalks, streets) without entering property at any time\n\n### Suspicious Activity Indicators (Level 1)\n- **Checking or probing vehicle/building access**: trying handles without entering, peering through windows, examining multiple vehicles, or possessing break-in tools \u2014 Level 1\n- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** \u2014 ALWAYS Level 1 regardless of activity or duration\n- Taking items that don't belong to them (packages, objects from porches/driveways)\n- Climbing or jumping fences/barriers to access property\n- Attempting to conceal actions or items from view\n- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence\n\n### Critical Threat Indicators (Level 2)\n- Holding break-in tools (crowbars, pry bars, bolt cutters)\n- Weapons visible (guns, knives, bats used aggressively)\n- Forced entry in progress\n- Physical aggression or violence\n- Active property damage or theft in progress\n\n### Assessment Guidance\nEvaluate in this order:\n\n1. **If person is verified/known** \u2192 Level 0 regardless of time or activity\n2. **If person is unidentified:**\n - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) \u2192 Level 1\n - Check actions: If probing access (trying handles without entering, checking multiple vehicles), taking items, climbing \u2192 Level 1\n - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service, routine vehicle access) \u2192 Level 0\n3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1)\n\nThe mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is."}}, "snapshots": {"enabled": false, "timestamp": false, "bounding_box": true, "crop": false, "required_zones": [], "height": null, "retain": {"default": 10, "mode": "motion", "objects": {}}, "quality": 60}, "timestamp_style": {"position": "tl", "format": "%m/%d/%Y %H:%M:%S", "color": {"red": 255, "green": 255, "blue": 255}, "thickness": 2, "effect": null}, "audio_transcription": {"enabled": false, "language": "en", "device": "CPU", "model_size": "small", "live_enabled": false}, "classification": {"bird": {"enabled": false, "threshold": 0.9}, "custom": {}}, "semantic_search": {"enabled": false, "reindex": false, "model": "jinav1", "model_size": "small", "device": null}, "face_recognition": {"enabled": false, "model_size": "small", "unknown_score": 0.8, "detection_threshold": 0.7, "recognition_threshold": 0.9, "min_area": 750, "min_faces": 1, "save_attempts": 200, "blur_confidence_filter": true, "device": null}, "lpr": {"enabled": false, "model_size": "small", "detection_threshold": 0.7, "min_area": 1000, "recognition_threshold": 0.9, "min_plate_length": 4, "format": null, "match_distance": 1, "known_plates": {}, "enhancement": 0, "debug_save_plates": false, "device": null, "replace_rules": []}, "camera_groups": {"default": {"cameras": ["front_door", "backyard", "garage"], "icon": "generic", "order": 0}, "outdoor": {"cameras": ["front_door", "backyard"], "icon": "generic", "order": 1}}, "profiles": {}} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/events.json b/web/e2e/fixtures/mock-data/events.json index a50c1d7bc7..fa698a9b41 100644 --- a/web/e2e/fixtures/mock-data/events.json +++ b/web/e2e/fixtures/mock-data/events.json @@ -1 +1 @@ -[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1775487131.3863528, "end_time": 1775487161.3863528, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1775483531.3863528, "end_time": 1775483576.3863528, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1775479931.3863528, "end_time": 1775479951.3863528, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}] \ No newline at end of file +[{"id": "event-person-001", "label": "person", "sub_label": null, "camera": "front_door", "start_time": 1780677009.365581, "end_time": 1780677039.365581, "false_positive": false, "zones": ["front_yard"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "abc123", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.92, "score": 0.92, "region": [0.1, 0.1, 0.5, 0.8], "box": [0.2, 0.15, 0.45, 0.75], "area": 0.18, "ratio": 0.6, "type": "object", "description": "A person walking toward the front door", "average_estimated_speed": 1.2, "velocity_angle": 45.0, "path_data": [[[0.2, 0.5], 0.0], [[0.3, 0.5], 1.0]]}}, {"id": "event-car-001", "label": "car", "sub_label": null, "camera": "backyard", "start_time": 1780673409.365581, "end_time": 1780673454.365581, "false_positive": false, "zones": ["driveway"], "thumbnail": null, "has_clip": true, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "def456", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.87, "score": 0.87, "region": [0.3, 0.2, 0.9, 0.7], "box": [0.35, 0.25, 0.85, 0.65], "area": 0.2, "ratio": 1.25, "type": "object", "description": "A car parked in the driveway", "average_estimated_speed": 0.0, "velocity_angle": 0.0, "path_data": []}}, {"id": "event-person-002", "label": "person", "sub_label": null, "camera": "garage", "start_time": 1780669809.365581, "end_time": 1780669829.365581, "false_positive": false, "zones": [], "thumbnail": null, "has_clip": false, "has_snapshot": true, "retain_indefinitely": false, "plus_id": null, "model_hash": "ghi789", "detector_type": "cpu", "model_type": "ssd", "data": {"top_score": 0.78, "score": 0.78, "region": [0.0, 0.0, 0.6, 0.9], "box": [0.1, 0.05, 0.5, 0.85], "area": 0.32, "ratio": 0.5, "type": "object", "description": null, "average_estimated_speed": 0.5, "velocity_angle": 90.0, "path_data": [[[0.1, 0.4], 0.0]]}}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/exports.json b/web/e2e/fixtures/mock-data/exports.json index 9af04f45a8..7e6c787088 100644 --- a/web/e2e/fixtures/mock-data/exports.json +++ b/web/e2e/fixtures/mock-data/exports.json @@ -1 +1 @@ -[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1775490731.3863528, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1775483531.3863528, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1775492531.3863528, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}] \ No newline at end of file +[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1780680609.365581, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1780673409.365581, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1780682409.365581, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}] \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/generate-mock-data.py b/web/e2e/fixtures/mock-data/generate-mock-data.py index aa96494b92..bba488d3d9 100644 --- a/web/e2e/fixtures/mock-data/generate-mock-data.py +++ b/web/e2e/fixtures/mock-data/generate-mock-data.py @@ -111,6 +111,18 @@ def generate_config(): return snapshot +def generate_config_schema(): + """Generate the JSON Schema for FrigateConfig from the backend model. + + This is what the app fetches from /api/config/schema.json to drive the + RJSF-based config form. Generating it here keeps the e2e fixture in sync + with the backend whenever config models change. + """ + from frigate.config import FrigateConfig + + return FrigateConfig.model_json_schema() + + def generate_reviews(): """Generate ReviewSegmentResponse[] validated against Pydantic + Peewee.""" from frigate.api.defs.response.review_response import ReviewSegmentResponse @@ -411,6 +423,7 @@ def main(): print() write_json("config-snapshot.json", generate_config()) + write_json("config-schema.json", generate_config_schema()) write_json("reviews.json", generate_reviews()) write_json("events.json", generate_events()) write_json("exports.json", generate_exports()) diff --git a/web/e2e/fixtures/mock-data/review-summary.json b/web/e2e/fixtures/mock-data/review-summary.json index ba54df37c9..bb3afc2ea3 100644 --- a/web/e2e/fixtures/mock-data/review-summary.json +++ b/web/e2e/fixtures/mock-data/review-summary.json @@ -1 +1 @@ -{"2026-04-06": {"day": "2026-04-06", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-04-05": {"day": "2026-04-05", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}} \ No newline at end of file +{"2026-06-05": {"day": "2026-06-05", "reviewed_alert": 1, "reviewed_detection": 0, "total_alert": 2, "total_detection": 2}, "2026-06-04": {"day": "2026-06-04", "reviewed_alert": 3, "reviewed_detection": 2, "total_alert": 3, "total_detection": 4}} \ No newline at end of file diff --git a/web/e2e/fixtures/mock-data/reviews.json b/web/e2e/fixtures/mock-data/reviews.json index 4930f01598..0b60850cfa 100644 --- a/web/e2e/fixtures/mock-data/reviews.json +++ b/web/e2e/fixtures/mock-data/reviews.json @@ -1 +1 @@ -[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-04-06T09:52:11.386353", "end_time": "2026-04-06T09:52:41.386353", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-04-06T08:52:11.386353", "end_time": "2026-04-06T08:52:56.386353", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-04-06T07:52:11.386353", "end_time": "2026-04-06T07:52:31.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-04-06T06:52:11.386353", "end_time": "2026-04-06T06:52:26.386353", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}] \ No newline at end of file +[{"id": "review-alert-001", "camera": "front_door", "start_time": "2026-06-05T11:30:09.365581", "end_time": "2026-06-05T11:30:39.365581", "has_been_reviewed": false, "severity": "alert", "thumb_path": "/clips/front_door/review-alert-001-thumb.jpg", "data": {"audio": [], "detections": ["person-abc123"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}, {"id": "review-alert-002", "camera": "backyard", "start_time": "2026-06-05T10:30:09.365581", "end_time": "2026-06-05T10:30:54.365581", "has_been_reviewed": true, "severity": "alert", "thumb_path": "/clips/backyard/review-alert-002-thumb.jpg", "data": {"audio": [], "detections": ["car-def456"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["driveway"]}}, {"id": "review-detect-001", "camera": "garage", "start_time": "2026-06-05T09:30:09.365581", "end_time": "2026-06-05T09:30:29.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/garage/review-detect-001-thumb.jpg", "data": {"audio": [], "detections": ["person-ghi789"], "objects": ["person"], "sub_labels": [], "significant_motion_areas": [], "zones": []}}, {"id": "review-detect-002", "camera": "front_door", "start_time": "2026-06-05T08:30:09.365581", "end_time": "2026-06-05T08:30:24.365581", "has_been_reviewed": false, "severity": "detection", "thumb_path": "/clips/front_door/review-detect-002-thumb.jpg", "data": {"audio": [], "detections": ["car-jkl012"], "objects": ["car"], "sub_labels": [], "significant_motion_areas": [], "zones": ["front_yard"]}}] \ No newline at end of file diff --git a/web/e2e/specs/chat.spec.ts b/web/e2e/specs/chat.spec.ts index fcd10782c1..cab49828ab 100644 --- a/web/e2e/specs/chat.spec.ts +++ b/web/e2e/specs/chat.spec.ts @@ -92,6 +92,15 @@ test.describe("Chat — streaming @medium", () => { await installChatStreamOverride(frigateApp, [ { type: "content", delta: "Hel" }, { type: "content", delta: "lo" }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "hello chat" }, + { role: "assistant", content: "Hello" }, + ], + }, + { type: "done" }, ]); await frigateApp.goto("/chat"); const input = frigateApp.page.getByPlaceholder(/ask/i); @@ -137,6 +146,15 @@ test.describe("Chat — streaming @medium", () => { { type: "content", delta: "Hel" }, { type: "content", delta: "lo, " }, { type: "content", delta: "world!" }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "greet me" }, + { role: "assistant", content: "Hello, world!" }, + ], + }, + { type: "done" }, ], { chunkDelayMs: 50 }, ); @@ -151,19 +169,39 @@ test.describe("Chat — streaming @medium", () => { }); }); - test("tool_calls chunks render a ToolCallsGroup", async ({ frigateApp }) => { - await installChatStreamOverride(frigateApp, [ + test("tool calls in the chain render a ToolCallsGroup", async ({ + frigateApp, + }) => { + const toolTurn = [ + { role: "system", content: "sys" }, + { role: "user", content: "find people" }, { - type: "tool_calls", + role: "assistant", + content: null, tool_calls: [ { id: "call_1", - name: "search_objects", - arguments: { label: "person" }, + type: "function", + function: { + name: "search_objects", + arguments: '{"label":"person"}', + }, }, ], }, + { role: "tool", tool_call_id: "call_1", content: "[]" }, + ]; + await installChatStreamOverride(frigateApp, [ + { type: "messages", messages: toolTurn }, { type: "content", delta: "Searching for people." }, + { + type: "messages", + messages: [ + ...toolTurn, + { role: "assistant", content: "Searching for people." }, + ], + }, + { type: "done" }, ]); await frigateApp.goto("/chat"); const input = frigateApp.page.getByPlaceholder(/ask/i); @@ -253,6 +291,15 @@ test.describe("Chat — attachment chip @medium", () => { // We use the stream override so the first message completes quickly. await installChatStreamOverride(frigateApp, [ { type: "content", delta: "Done." }, + { + type: "messages", + messages: [ + { role: "system", content: "sys" }, + { role: "user", content: "hello" }, + { role: "assistant", content: "Done." }, + ], + }, + { type: "done" }, ]); await frigateApp.goto("/chat"); diff --git a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts new file mode 100644 index 0000000000..38b0f5c4a6 --- /dev/null +++ b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts @@ -0,0 +1,203 @@ +/** + * Camera ffmpeg streams settings tests -- MEDIUM tier. + * + * Covers the input-path source toggle: each ffmpeg input can either point at a + * go2rtc restream (picked from a dropdown, which writes the rtsp://127.0.0.1:8554 + * path plus the preset-rtsp-restream input_args) or use a manually typed path. + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; +import { configFactory } from "../../fixtures/mock-data/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_SCHEMA = JSON.parse( + readFileSync( + resolve(__dirname, "../../fixtures/mock-data/config-schema.json"), + "utf-8", + ), +); + +const GO2RTC_STREAMS = { + dome_main: ["rtsp://user:pass@192.168.0.20:554/Stream1"], + dome_sub: ["rtsp://user:pass@192.168.0.20:554/Stream2"], +}; + +type CameraInput = { + path: string; + roles: string[]; + input_args?: string; +}; + +async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) { + const config = configFactory({ + go2rtc: { streams: GO2RTC_STREAMS }, + cameras: { + front_door: { + ffmpeg: { inputs: frontDoorInputs }, + }, + }, + }); + + let lastSavedConfig: unknown = null; + + await page.route("**/api/config/schema.json", (route) => + route.fulfill({ json: CONFIG_SCHEMA }), + ); + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ + json: { + cameras: { front_door: { ffmpeg: { inputs: frontDoorInputs } } }, + go2rtc: { streams: GO2RTC_STREAMS }, + }, + }), + ); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + await page.route("**/api/ffmpeg/presets", (route) => + route.fulfill({ + json: { + hwaccel_args: [], + input_args: ["preset-rtsp-restream", "preset-rtsp-generic"], + output_args: { record: [], detect: [] }, + }, + }), + ); + + return { capturedConfig: () => lastSavedConfig }; +} + +const RESTREAM_RADIO = "Restream (go2rtc)"; +const MANUAL_RADIO = "Manual input path"; + +test.describe("camera ffmpeg input source toggle @medium", () => { + test("manual input defaults to the manual text field", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }), + ).toBeChecked(); + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://10.0.0.1:554/video"); + }); + + test("an existing restream path auto-detects into restream mode", async ({ + frigateApp, + }) => { + await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await expect( + frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }), + ).toBeChecked(); + // The dropdown is preselected to the matching go2rtc stream. + await expect( + frigateApp.page.getByRole("combobox", { name: /go2rtc stream/i }), + ).toContainText("dome_main"); + }); + + test("selecting a restream writes the path and preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { path: "rtsp://10.0.0.1:554/video", roles: ["detect"] }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: RESTREAM_RADIO }).click(); + await frigateApp.page + .getByRole("combobox", { name: /go2rtc stream/i }) + .click(); + + // The dropdown is searchable: typing narrows the list to matches only, + // with no option to enter a custom stream name. + await frigateApp.page.getByPlaceholder("Search streams...").fill("sub"); + await expect( + frigateApp.page.getByRole("option", { name: "dome_main" }), + ).toBeHidden(); + await frigateApp.page.getByRole("option", { name: "dome_sub" }).click(); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + cameras: { + front_door: { + ffmpeg: { + inputs: [ + { + path: "rtsp://127.0.0.1:8554/dome_sub", + input_args: "preset-rtsp-restream", + }, + ], + }, + }, + }, + }, + }); + }); + + test("switching a restream back to manual reverts the preset", async ({ + frigateApp, + }) => { + const capture = await installRoutes(frigateApp.page, [ + { + path: "rtsp://127.0.0.1:8554/dome_main", + roles: ["detect"], + input_args: "preset-rtsp-restream", + }, + ]); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + await frigateApp.page.getByRole("radio", { name: MANUAL_RADIO }).click(); + + // The restream path stays editable in the manual text field. + await expect( + frigateApp.page.getByRole("textbox", { name: "Input path" }), + ).toHaveValue("rtsp://127.0.0.1:8554/dome_main"); + + await frigateApp.page.getByRole("button", { name: "Save" }).click(); + + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .not.toBeNull(); + + const payload = capture.capturedConfig() as { + config_data?: { + cameras?: { + front_door?: { + ffmpeg?: { inputs?: Array<{ input_args?: unknown }> }; + }; + }; + }; + }; + const input = + payload?.config_data?.cameras?.front_door?.ffmpeg?.inputs?.[0]; + expect(input?.input_args).not.toBe("preset-rtsp-restream"); + }); +}); diff --git a/web/public/locales/ar/audio.json b/web/public/locales/ar/audio.json index 19bbfddeac..350d644a71 100644 --- a/web/public/locales/ar/audio.json +++ b/web/public/locales/ar/audio.json @@ -149,5 +149,17 @@ "cluck": "قرقرة", "cock_a_doodle_doo": "كوكو-كو-كوووووو", "turkey": "ديك رومى", - "gobble": "كركرة" + "gobble": "كركرة", + "tearing": "يمزق", + "ping": "طنّة", + "clang": "صوت رنين", + "squeal": "يصرخ", + "creak": "صرير", + "sizzle": "صوت الأزيز", + "clicking": "النقر", + "clickety_clack": "طقطقة", + "rumble": "الحلبة", + "skateboard": "لوح تزلج", + "echo": "صدى الصوت", + "noise": "ازعاج" } diff --git a/web/public/locales/ar/objects.json b/web/public/locales/ar/objects.json index 03bf58ab19..50d19ad341 100644 --- a/web/public/locales/ar/objects.json +++ b/web/public/locales/ar/objects.json @@ -22,5 +22,6 @@ "mouse": "فأر", "keyboard": "لوحة المفاتيح", "goat": "معزة", - "sheep": "غنم" + "sheep": "غنم", + "skateboard": "لوح تزلج" } diff --git a/web/public/locales/bg/audio.json b/web/public/locales/bg/audio.json index 8eab73ef19..0380caf31b 100644 --- a/web/public/locales/bg/audio.json +++ b/web/public/locales/bg/audio.json @@ -355,8 +355,43 @@ "steam_whistle": "Парна свирка", "mechanisms": "Механизми", "clock": "Часовник", - "tick": "", + "tick": "Тик", "tick-tock": "Тиктакане", "gears": "Зъбни колела", - "sewing_machine": "Шиеща машина" + "sewing_machine": "Шиеща машина", + "sound_effect": "Звуков ефект", + "tubular_bells": "Тръбни камбани", + "mallet_percussion": "Ударни чукчета", + "marimba": "Маримба", + "glockenspiel": "Металлофон", + "steelpan": "Пан барабан", + "string_section": "Струнна група", + "jingle_bell": "Звънче", + "chime": "Звънече", + "wind_chime": "Вятърен звън", + "singing_bowl": "Пееща купа", + "ambient_music": "Амбиентна музика", + "new-age_music": "Музика от ново поколение", + "vocal_music": "Вокална музика", + "gurgling": "Бълбукане", + "light_engine": "Лек двигател", + "medium_engine": "Среден двигател", + "heavy_engine": "Тежък двигател", + "ratchet": "Тресчотка", + "hammer": "Ръчен чук", + "pulleys": "Макари", + "mechanical_fan": "Механичен вентилатор", + "air_conditioning": "Климатик", + "cash_register": "Каса", + "printer": "Принтер", + "camera": "Камера", + "tools": "Инструменти", + "artillery_fire": "Артилерийски огън", + "boom": "Гръм", + "glass": "Стъкло", + "crack": "Пукнатина", + "wood": "Дърво", + "silence": "Тишина", + "liquid": "Течност", + "splash": "Разливам" } diff --git a/web/public/locales/bg/common.json b/web/public/locales/bg/common.json index 94e85ddd96..fcecbc20a6 100644 --- a/web/public/locales/bg/common.json +++ b/web/public/locales/bg/common.json @@ -63,13 +63,17 @@ "untilRestart": "До рестарт", "mo": "{{time}}мес", "m": "{{time}}м", - "s": "{{time}}с" + "s": "{{time}}с", + "never": "Никога", + "formattedTimestampMonthDayYearHourMinute": { + "12hour": "d MMM yyyy, h:mm aaa" + } }, "button": { "apply": "Приложи", "reset": "Нулиране", "done": "Готово", - "disabled": "Деактивирано", + "disabled": "Изключено", "save": "Запази", "saving": "Запазване…", "cancel": "Отказ", @@ -80,7 +84,7 @@ "delete": "Изтриване", "yes": "Да", "download": "Изтегляне", - "enabled": "Активирано", + "enabled": "Включено", "history": "История", "back": "Назад", "fullscreen": "Цял екран", @@ -99,8 +103,8 @@ "export": "Експортиране", "deleteNow": "Изтрии сега", "next": "Следващ", - "disable": "Деактивирай", - "enable": "Активирай" + "disable": "Изключи", + "enable": "Включи" }, "menu": { "live": { diff --git a/web/public/locales/bg/components/filter.json b/web/public/locales/bg/components/filter.json index 3aa7b61cea..afb81bb13c 100644 --- a/web/public/locales/bg/components/filter.json +++ b/web/public/locales/bg/components/filter.json @@ -30,5 +30,10 @@ "title": "Всички дати", "short": "Дати" } - } + }, + "more": "Още филтри", + "reset": { + "label": "Рестартирай филтрите по подразбиране" + }, + "timeRange": "Времеви диапазон" } diff --git a/web/public/locales/bg/components/player.json b/web/public/locales/bg/components/player.json index 39d9699d77..706f819b31 100644 --- a/web/public/locales/bg/components/player.json +++ b/web/public/locales/bg/components/player.json @@ -34,7 +34,8 @@ }, "submitFrigatePlus": { "title": "Да се изпрати ли този кадър към Frigate+?", - "submit": "Изпрати" + "submit": "Изпрати", + "previewError": "Не можe да се зареди предварителен преглед на моментната снимка. Записът може да не е наличен в момента." }, "noPreviewFound": "Не е намерен предварителен преглед", "noRecordingsFoundForThisTime": "За това време не са намерени записи", @@ -47,5 +48,6 @@ "error": { "submitFrigatePlusFailed": "Неуспешно изпратен кадър към Frigate+" } - } + }, + "cameraOff": "Камерата е изключена" } diff --git a/web/public/locales/bg/config/cameras.json b/web/public/locales/bg/config/cameras.json index 0967ef424b..bdace6a15c 100644 --- a/web/public/locales/bg/config/cameras.json +++ b/web/public/locales/bg/config/cameras.json @@ -1 +1,69 @@ -{} +{ + "label": "Конфигурация на камерата", + "timestamp_style": { + "label": "Стил на времева щампа", + "format": { + "label": "Формат на времева щампа" + }, + "color": { + "red": { + "label": "Червено", + "description": "Червен компонент (0-255) за времева щампа." + }, + "green": { + "description": "Зелен компонент (0-255) за времева щампа.", + "label": "Зелено" + }, + "blue": { + "description": "Син компонент (0-255) за времева щампа.", + "label": "Синьо" + }, + "label": "Цвят на времева щампа", + "description": "RGB стойности за текста на времева щампа (всички стойности 0-255)." + }, + "thickness": { + "description": "Дебелина на текстовата линия за времева щампа.", + "label": "Дебелина на времева щампа" + }, + "effect": { + "label": "Ефект на времева щампа", + "description": "Визуален ефект на времева щампа (без, плътен, сянка)." + }, + "position": { + "label": "Позиция на времева щампа", + "description": "Позиция за времева щампа на снимката (гл/гд/дл/дд)." + } + }, + "audio_transcription": { + "label": "Аудио транскрипция", + "live_enabled": { + "label": "Транскрипция на живо" + } + }, + "snapshots": { + "quality": { + "label": "Качество на моментната снимка" + }, + "required_zones": { + "label": "Задължителни зони", + "description": "Зони в които обект трябва да влезе за да се запази моментна снимка." + }, + "height": { + "label": "Височина на моментната снимка" + }, + "retain": { + "default": { + "description": "Дни по подразбиране за задържане на моментните снимки." + } + } + }, + "semantic_search": { + "label": "Семантично търсене", + "triggers": { + "label": "Спусък", + "friendly_name": { + "label": "Удобно име" + } + } + } +} diff --git a/web/public/locales/bg/config/global.json b/web/public/locales/bg/config/global.json index ad191cd667..85376b2f76 100644 --- a/web/public/locales/bg/config/global.json +++ b/web/public/locales/bg/config/global.json @@ -4,5 +4,148 @@ "session_length": { "label": "Продължителност на сесията" } + }, + "version": { + "label": "Текуща версия на конфигурацията", + "description": "Версия на активната конфигурация. Помага за проследяване на промени от миграция или форматиране." + }, + "safe_mode": { + "label": "Безопасен режим", + "description": "При избор, Frigate ще стартира в безопасен режим за отстраняване на неизправности." + }, + "environment_vars": { + "label": "Променливи", + "description": "Параметри за стартиране на Frigate в Home Assistant OS. Non-HAOS потребителите трябва да използват Docker конфигурация." + }, + "logger": { + "label": "Логове", + "default": { + "label": "Ниво на логовете" + } + }, + "timestamp_style": { + "label": "Стил на времева щампа", + "format": { + "label": "Формат на времева щампа" + }, + "color": { + "red": { + "label": "Червено", + "description": "Червен компонент (0-255) за времева щампа." + }, + "green": { + "description": "Зелен компонент (0-255) за времева щампа.", + "label": "Зелено" + }, + "blue": { + "description": "Син компонент (0-255) за времева щампа.", + "label": "Синьо" + }, + "label": "Цвят на времева щампа", + "description": "RGB стойности за текста на времева щампа (всички стойности 0-255)." + }, + "thickness": { + "description": "Дебелина на текстовата линия за времева щампа.", + "label": "Дебелина на времева щампа" + }, + "effect": { + "label": "Ефект на времева щампа", + "description": "Визуален ефект на времева щампа (без, плътен, сянка)." + }, + "position": { + "label": "Позиция на времева щампа", + "description": "Позиция за времева щампа на снимката (гл/гд/дл/дд)." + } + }, + "audio_transcription": { + "label": "Аудио транскрипция", + "enabled": { + "label": "Включи аудио транскрипцията" + }, + "language": { + "label": "Език на транскрипция" + }, + "device": { + "label": "Устройство за транскрипция" + }, + "model_size": { + "label": "Големина на модела" + }, + "live_enabled": { + "label": "Транскрипция на живо" + } + }, + "snapshots": { + "quality": { + "label": "Качество на моментната снимка" + }, + "required_zones": { + "label": "Задължителни зони", + "description": "Зони в които обект трябва да влезе за да се запази моментна снимка." + }, + "height": { + "label": "Височина на моментната снимка" + }, + "retain": { + "default": { + "description": "Дни по подразбиране за задържане на моментните снимки." + } + } + }, + "classification": { + "label": "Класификация на обекти", + "bird": { + "label": "Конфигурация за класификация на птици", + "enabled": { + "label": "Класификация на птици", + "description": "Клацификация на птици (Вкл./Изкл.)." + }, + "threshold": { + "label": "Минимален резултат" + } + }, + "custom": { + "label": "Потребителски класификационни модели", + "enabled": { + "label": "Включи модел", + "description": "Потребителски класификационнен модел (Вкл./Изкл.)." + }, + "name": { + "label": "Име на модел" + }, + "save_attempts": { + "label": "Опити за запазване" + }, + "state_config": { + "motion": { + "label": "Изпълни при движение" + }, + "interval": { + "label": "Интервал за класификация" + } + } + } + }, + "semantic_search": { + "label": "Семантично търсене", + "enabled": { + "label": "Включи семантично търсене", + "description": "Семантично търсене (Вкл./Изкл.)." + }, + "reindex": { + "label": "Реиндексирай при стартиране" + }, + "model_size": { + "label": "Големина на модел" + }, + "device": { + "label": "Устройство" + }, + "triggers": { + "label": "Спусък", + "friendly_name": { + "label": "Удобно име" + } + } } } diff --git a/web/public/locales/bg/objects.json b/web/public/locales/bg/objects.json index c89c359fb3..79485a726f 100644 --- a/web/public/locales/bg/objects.json +++ b/web/public/locales/bg/objects.json @@ -24,5 +24,6 @@ "hair_dryer": "Сешоар", "toothbrush": "Четка за зъби", "scissors": "Ножица", - "clock": "Часовник" + "clock": "Часовник", + "airplane": "Самолет" } diff --git a/web/public/locales/bg/views/classificationModel.json b/web/public/locales/bg/views/classificationModel.json index 7b8ecb1dd6..493cf18bcb 100644 --- a/web/public/locales/bg/views/classificationModel.json +++ b/web/public/locales/bg/views/classificationModel.json @@ -2,5 +2,13 @@ "documentTitle": "Модели за класификация - Frigate", "description": { "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." + }, + "details": { + "scoreInfo": "Резултатът представлява средната степен на увереност в класификацията при всички засечки на този обект." + }, + "wizard": { + "step1": { + "classificationAttribute": "Атрибут" + } } } diff --git a/web/public/locales/bg/views/events.json b/web/public/locales/bg/views/events.json index affd0cb52e..bf0c0a76c7 100644 --- a/web/public/locales/bg/views/events.json +++ b/web/public/locales/bg/views/events.json @@ -14,5 +14,6 @@ "detections": "Засичания", "motion": { "label": "Движение" - } + }, + "camera": "Камера" } diff --git a/web/public/locales/bg/views/explore.json b/web/public/locales/bg/views/explore.json index d6c074d4e1..84a80602ed 100644 --- a/web/public/locales/bg/views/explore.json +++ b/web/public/locales/bg/views/explore.json @@ -5,10 +5,69 @@ "mismatch_one": "{{count}} недостъпен обект беше открит и включен в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити.", "mismatch_other": "{{count}} недостъпни обекта бяха открити и включени в този елемент за преглед. Тези обекти или не са квалифицирани като предупреждение или откриване, или вече са били изчистени/изтрити." } + }, + "editLPR": { + "title": "Редактиране на регистрационния номер" + }, + "editAttributes": { + "title": "Редактиране на атрибутите" + }, + "topScore": { + "label": "Най-силен резултат" + }, + "estimatedSpeed": "Естимирана скорост", + "objects": "Обекти", + "camera": "Камера", + "zones": "Зони", + "timestamp": "Времева щампа", + "button": { + "findSimilar": "Намери подобни", + "regenerate": { + "title": "Регенерирай", + "label": "Регенерирай описанието на следените обекти" + } + }, + "description": { + "label": "Описание", + "placeholder": "Описание на следените обекти" } }, "trackedObjectsCount_one": "{{count}} проследен обект ", "trackedObjectsCount_other": "{{count}} проследени обекта ", "documentTitle": "Разгледай - Фригейт", - "generativeAI": "Генеративен Изкъствен Интелект" + "generativeAI": "Генеративен Изкъствен Интелект", + "itemMenu": { + "downloadSnapshot": { + "aria": "Сваляне на моментна снимка/кадър" + }, + "viewTrackingDetails": { + "label": "Виж детайли за следенето", + "aria": "Покажи детайли за следенето" + }, + "findSimilar": { + "label": "Намери подобни" + }, + "submitToPlus": { + "label": "Изпрати към Frigate+", + "aria": "Изпрати към Frigate Plus" + }, + "viewInHistory": { + "label": "Виж в история", + "aria": "Виж в история" + }, + "more": { + "aria": "Повече" + } + }, + "dialog": { + "confirmDelete": { + "title": "Потвърди изтриване" + } + }, + "aiAnalysis": { + "title": "AI Анализ" + }, + "concerns": { + "label": "Притеснения" + } } diff --git a/web/public/locales/bg/views/exports.json b/web/public/locales/bg/views/exports.json index 5454a085d6..344695c2d1 100644 --- a/web/public/locales/bg/views/exports.json +++ b/web/public/locales/bg/views/exports.json @@ -17,7 +17,58 @@ }, "toast": { "error": { - "renameExportFailed": "Неуспешно преименуване на експорт: {{errorMessage}}" + "renameExportFailed": "Неуспешно преименуване на експорт: {{errorMessage}}", + "assignCaseFailed": "Неуспешно обновяване на възложен случай: {{errorMessage}}", + "caseSaveFailed": "Неуспешно запазен случай: {{errorMessage}}", + "caseDeleteFailed": "Неуспешно изтрит случай: {{errorMessage}}" + } + }, + "deleteCase": { + "desc": "Сигурен ли сте, че искате да изтриете {{caseName}}?", + "label": "Изтрии случай" + }, + "caseDialog": { + "nameLabel": "Име на случай", + "descriptionLabel": "Описание" + }, + "toolbar": { + "editCase": "Редактирай случай", + "deleteCase": "Изтрии случай" + }, + "caseCard": { + "emptyCase": "Все още няма експорти" + }, + "jobCard": { + "defaultName": "{{camera}} експортиране", + "queued": "На опашката", + "running": "В ход", + "preparing": "Подготвяне", + "copying": "Копиране", + "encoding": "Енкодиране", + "encodingRetry": "Енкодиране (повторно)", + "finalizing": "Финализиране" + }, + "caseView": { + "noDescription": "Без описание", + "createdAt": "Създаден {{value}}", + "exportCount_one": "1 експорт", + "exportCount_other": "{{count}} експорти", + "cameraCount_one": "1 камера", + "cameraCount_other": "{{count}} камери", + "showMore": "Покажи повече", + "showLess": "Покажи по-малко" + }, + "bulkActions": { + "delete": "Изтрии", + "deleteNow": "Изтрии сега" + }, + "bulkDelete": { + "title": "Изтрии експорти", + "desc_one": "Сигурни ли сте , че искате да изтриете {{count}} експорта?" + }, + "bulkToast": { + "success": { + "delete": "Успешно изтрити експорти" } } } diff --git a/web/public/locales/bg/views/faceLibrary.json b/web/public/locales/bg/views/faceLibrary.json index 7d4a82211c..d00942b779 100644 --- a/web/public/locales/bg/views/faceLibrary.json +++ b/web/public/locales/bg/views/faceLibrary.json @@ -15,5 +15,8 @@ "addFace": "Добавете нова колекция във библиотеката за лица при качването на първата ви снимка.", "placeholder": "Напишете име за тази колекция", "invalidName": "Невалидно име. Имената могат да съдържат единствено: букви, числа, празни места, долни черти и тирета." + }, + "details": { + "timestamp": "Времева щампа" } } diff --git a/web/public/locales/bg/views/live.json b/web/public/locales/bg/views/live.json index 01b3a5c34f..c10906c71d 100644 --- a/web/public/locales/bg/views/live.json +++ b/web/public/locales/bg/views/live.json @@ -16,7 +16,8 @@ "move": { "clickMove": { "enable": "Включи кликване за преместване", - "disable": "Изключи кликване за преместване" + "disable": "Изключи кликване за преместване", + "label": "Кликнете в центъра на кадъра за да центрирате камерата" } } }, @@ -26,7 +27,8 @@ }, "recording": { "enable": "Включи запис", - "disable": "Изключи запис" + "disable": "Изключи запис", + "disabledInConfig": "\"Записване\" трябва първо да се вкючи през настройките за тази камера." }, "snapshots": { "enable": "Включи моментни снимки", @@ -38,7 +40,9 @@ }, "camera": { "enable": "Включи камера", - "disable": "Изключи камера" + "disable": "Изключи камера", + "turnOn": "Включване на камера", + "turnOff": "Изключване на камера" }, "detect": { "enable": "Включи разпознаване", @@ -65,5 +69,16 @@ "cameraEnabled": "Камерата е включена" }, "documentTitle": "Наживо - Frigate", - "documentTitle.withCamera": "{{camera}} - На живо - Фригейт" + "documentTitle.withCamera": "{{camera}} - На живо - Фригейт", + "noCameras": { + "default": { + "buttonText": "Добави камера" + }, + "group": { + "title": "Няма камери в групата", + "description": "Тази група няма добавени или включени камери.", + "buttonText": "Управление на групите" + } + }, + "lowBandwidthMode": "Режим ограничена/бавна връзка" } diff --git a/web/public/locales/ca/components/dialog.json b/web/public/locales/ca/components/dialog.json index 6f527e4df7..f63d47f74f 100644 --- a/web/public/locales/ca/components/dialog.json +++ b/web/public/locales/ca/components/dialog.json @@ -24,6 +24,9 @@ }, "state": { "submitted": "Enviat" + }, + "toast": { + "error": "No s'ha pogut enviar a Frigate+. Si us plau, comproveu la vostra connexió de xarxa i torneu-ho a provar." } } }, @@ -107,7 +110,14 @@ "queueingButton": "S'estan posant a la cua les exportacions...", "exportButton_one": "Exporta 1 càmera", "exportButton_many": "Exporta {{count}} càmeres", - "exportButton_other": "Exporta {{count}} càmeres" + "exportButton_other": "Exporta {{count}} càmeres", + "searchOrSelectGroup": "Cerca, o selecciona un grup de càmeres...", + "selectAll": "Selecciona totes les càmeres", + "clearSelection": "Neteja la selecció", + "selectWithActivity": "Càmeres amb objectes rastrejats", + "selectGroup": "Selecciona un grup", + "noMatchingCameras": "No hi ha càmeres que coincideixin amb la cerca", + "selectedCount": "{{selected}} / {{total}} seleccionats" }, "multi": { "title_one": "Exporta {{count}} ressenyes", diff --git a/web/public/locales/ca/config/cameras.json b/web/public/locales/ca/config/cameras.json index aa3fb9a778..1ba68b11cd 100644 --- a/web/public/locales/ca/config/cameras.json +++ b/web/public/locales/ca/config/cameras.json @@ -29,7 +29,7 @@ }, "listen": { "label": "Tipus d'escoltes", - "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, crit, parla, crida)." + "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, parla, crida)." }, "filters": { "label": "Filtres d'àudio", @@ -152,11 +152,11 @@ } }, "ffmpeg": { - "label": "FFmpeg", - "description": "Paràmetres del FFmpeg que inclouen la ruta dels binaris, args, opcions de hwaccel i args de sortida per rol.", + "label": "Fluxos (FFmpeg)", + "description": "Les entrades de flux de càmera i les opcions del FFmpeg, incloent-hi el camí binari, els arguments, l'hwaccel i els arguments de sortida per rol.", "path": { "label": "Ruta FFmpeg", - "description": "Ruta al binari FFmpeg a usar o un àlies de versió («5.0» o «7.0»)." + "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.0»)." }, "global_args": { "label": "Arguments globals del FFmpeg", diff --git a/web/public/locales/ca/config/global.json b/web/public/locales/ca/config/global.json index 2d8c1bff6b..038deb4d41 100644 --- a/web/public/locales/ca/config/global.json +++ b/web/public/locales/ca/config/global.json @@ -7,7 +7,7 @@ "description": "Paràmetres del FFmpeg que inclouen la ruta dels binaris, args, opcions de hwaccel i args de sortida per rol.", "path": { "label": "Ruta FFmpeg", - "description": "Ruta al binari FFmpeg a usar o un àlies de versió («5.0» o «7.0»)." + "description": "Ruta al binari FFmpeg a usar o un àlies de versió («7.0» o «8.0»)." }, "global_args": { "label": "Arguments globals del FFmpeg", @@ -2018,7 +2018,7 @@ }, "listen": { "label": "Tipus d'escoltes", - "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, crit, parla, crida)." + "description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, parla, crida)." }, "filters": { "label": "Filtres d'àudio", diff --git a/web/public/locales/ca/views/settings.json b/web/public/locales/ca/views/settings.json index 5ff0242d9a..97905e75cc 100644 --- a/web/public/locales/ca/views/settings.json +++ b/web/public/locales/ca/views/settings.json @@ -68,7 +68,7 @@ "integrationObjectClassification": "Classificació de l'objecte", "integrationAudioTranscription": "Transcripció d'àudio", "cameraDetect": "Detecció d'objectes", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Fluxos (FFmpeg)", "cameraRecording": "Enregistrament", "cameraSnapshots": "Instantànies", "cameraMotion": "Detecció de moviment", @@ -1689,7 +1689,17 @@ } }, "cameraInputs": { - "itemTitle": "Flux {{index}}" + "itemTitle": "Flux {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Camí d'entrada manual", + "go2rtcStreamLabel": "flux go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flux go2rtc", + "noGo2rtcStreams": "No s'ha configurat cap flux go2rtc", + "go2rtcStreamSearch": "Cerca fluxos...", + "availableStreams": "Fluxos disponibles", + "noMatchingStreams": "No hi ha fluxos coincidents" + } }, "restartRequiredField": "Reinicia requerit", "restartRequiredFooter": "S'ha canviat la configuració - es requereix reiniciar", @@ -2084,6 +2094,13 @@ }, "onvif": { "autotrackingNoZones": "Autotraquejar requereix al menys una zona. Defineix una zona per aquesta cámera a Mascares/Zones, després usa'l com a requerit a la part inferior." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "No es recomanen arguments manuals d'acceleració de maquinari. Tret que existeixi un requisit específic, seleccioneu el predefinit que coincideixi amb el vostre maquinari." + }, + "model": { + "optimizedFor320": "Frigate està optimitzada per a un model 320x320, que és la millor opció per a la majoria de configuracions. Un model 640x640 és més lent i només ajuda en escenaris específics.", + "inputDimensionsNotDetectResolution": "L'amplada i l'alçada del model d'entrada són les dimensions d'entrada del model de detecció d'objectes, no la resolució de detecció de la càmera. Haurien de coincidir amb les dimensions del model que esteu utilitzant - típicament una mida quadrada com 320x320 o 640x640." } }, "modelSize": { diff --git a/web/public/locales/ca/views/system.json b/web/public/locales/ca/views/system.json index 70b5ee6fe1..c472937c24 100644 --- a/web/public/locales/ca/views/system.json +++ b/web/public/locales/ca/views/system.json @@ -191,7 +191,22 @@ }, "audio": "Àudio:", "cameraProbeInfo": "Informació del sondeig de la càmera {{camera}}", - "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe." + "streamDataFromFFPROBE": "Les dades de la transmissió són obtingudes mitjançant ffprobe.", + "keyframes": { + "title": "Anàlisi de fotogrames clau", + "analyzing": "S'estan analitzant els fotogrames clau... queden {{seconds}} segons", + "stillAnalyzing": "Encara s'estan analitzant els fotogrames clau...", + "recordStream": "Registre de flux:", + "keyframeCount": "Fotogrames clau observats:", + "observedDuration": "Durada observada:", + "gap": "Espai de fotogrames clau (mín / avg / max):", + "segmentLength": "Longitud del segment d'enregistrament:", + "ok": "Fotogrames clau cada ,{{seconds}}s, bons per enregistrar i reproduir.", + "warning": "Els fotogrames clau dispersos o variables (espai més llarg .{{seconds}}s), probablement un còdec intel·ligent (H.264+/H.265+), això no és recomanable.", + "error": "El buit dels fotogrames clau ( the{{seconds}}s) excedeix la longitud del segment d'enregistrament ({{segmentTime}}s). Alguns segments poden no tenir un fotograma clau, el qual trenca la reproducció. Desactiva el còdec intel·ligent/+ a la càmera o escurça el seu interval de fotogrames clau.", + "unknown": "No s'ha pogut determinar l'espaiat dels fotogrames clau.", + "recordDisabled": "L'enregistrament està desactivat per a aquesta càmera." + } }, "title": "Càmeres", "overview": "Visió general", diff --git a/web/public/locales/da/audio.json b/web/public/locales/da/audio.json index 168ef4c679..0d73b074f8 100644 --- a/web/public/locales/da/audio.json +++ b/web/public/locales/da/audio.json @@ -180,7 +180,7 @@ "rats": "Rotter", "patter": "Dråbelyd", "insect": "Insekt", - "cricket": "Cricket", + "cricket": "Fårekylling", "guitar": "Guitar", "electric_guitar": "Elektrisk Guitar", "bass_guitar": "Basguitar", @@ -195,5 +195,16 @@ "rimshot": "Kantslag", "drum_roll": "Trommehvirvel", "bass_drum": "Stortromme", - "techno": "Techno" + "techno": "Techno", + "mosquito": "Myg", + "fly": "Flue", + "buzz": "Summen", + "frog": "Frø", + "croak": "Kvæk", + "snake": "Slange", + "rattle": "Klapren", + "whale_vocalization": "Hvallyde", + "music": "Musik", + "musical_instrument": "Musikinstrument", + "plucked_string_instrument": "Strengeinstrument" } diff --git a/web/public/locales/da/common.json b/web/public/locales/da/common.json index 62b6d70361..9b096cabad 100644 --- a/web/public/locales/da/common.json +++ b/web/public/locales/da/common.json @@ -24,7 +24,7 @@ "am": "am", "year_one": "{{time}} år", "year_other": "{{time}} år", - "mo": "{{time}}må", + "mo": "{{time}}md.", "month_one": "{{time}} måned", "month_other": "{{time}} måneder", "d": "{{time}}d", @@ -121,18 +121,18 @@ "back": "Tilbage", "history": "Historik", "fullscreen": "Fuldskærm", - "exitFullscreen": "Afslut Fludskærm", - "pictureInPicture": "Billede i Billede", - "twoWayTalk": "2 vejs samtale", - "cameraAudio": "Kamera Lyd", - "on": "ON", - "off": "OFF", + "exitFullscreen": "Afslut Fuldskærm", + "pictureInPicture": "Billede i billede", + "twoWayTalk": "Samtale", + "cameraAudio": "Kameralyd", + "on": "Til", + "off": "Fra", "edit": "Rediger", "copyCoordinates": "Kopier koordinater", "delete": "Slet", "yes": "Ja", "no": "Nej", - "download": "Download", + "download": "Hent", "info": "Info", "suspended": "Sat på pause", "unsuspended": "Genoptag", @@ -141,7 +141,19 @@ "export": "Eksporter", "deleteNow": "Slet nu", "next": "Næste", - "continue": "Fortsæt" + "continue": "Fortsæt", + "add": "Tilføj", + "applying": "Anvender…", + "undo": "Annuler", + "copiedToClipboard": "Kopieret til udklipsholder", + "modified": "Ændret", + "overridden": "Overskrevet", + "resetToGlobal": "Gendan til global", + "resetToDefault": "Gendan standard", + "saveAll": "Gem alle", + "savingAll": "Gemmer alle…", + "undoAll": "Fortryd alle", + "retry": "Prøv igen" }, "menu": { "system": "System", @@ -194,7 +206,9 @@ "gl": "Galego (Galisisk)", "id": "Bahasa Indonesia (Indonesisk)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (Kroatisk)" + "hr": "Hrvatski (Kroatisk)", + "zhHant": "繁體中文 (Traditionel Kinesisk)", + "bs": "Bosanski (Bosnisk)" }, "appearance": "Udseende", "darkMode": { @@ -243,7 +257,11 @@ "logout": "Log ud", "setPassword": "Vælg kodeord" }, - "classification": "Kategorisering" + "classification": "Kategorisering", + "profiles": "Profiler", + "actions": "Handlinger", + "features": "Funktioner", + "chat": "Chat" }, "toast": { "copyUrlToClipboard": "Kopieret URL til udklipsholder.", @@ -252,13 +270,14 @@ "error": { "title": "Ændringer kunne ikke gemmes: {{errorMessage}}", "noMessage": "Kunne ikke gemme konfigurationsændringer" - } + }, + "success": "Gemte ændringerne i konfigurationen." } }, "role": { "title": "Rolle", "admin": "Admin", - "viewer": "Viewer", + "viewer": "Seer", "desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI." }, "pagination": { @@ -296,5 +315,10 @@ }, "information": { "pixels": "{{area}}px" + }, + "no_items": "Intet fundet", + "validation_errors": "Valideringsfejl", + "credentialField": { + "savedPlaceholder": "Gemt - efterlad blank for at bevare nuværende" } } diff --git a/web/public/locales/da/components/camera.json b/web/public/locales/da/components/camera.json index cc5244b8a2..faff7a09ee 100644 --- a/web/public/locales/da/components/camera.json +++ b/web/public/locales/da/components/camera.json @@ -31,7 +31,7 @@ "setting": { "label": "Kamera Streaming Indstillinger", "title": "{{cameraName}} Streaming Indstillinger", - "desc": "Skift de live streaming muligheder for denne kameragruppes dashboard. Disse indstillinger er enheds- og browserspecifikke.", + "desc": "Skift indstillingerne for direkte visning af denne kameragruppes dashboard. Disse indstillinger er enheds- og browserspecifikke.", "audioIsAvailable": "Lyd er tilgængelig for denne stream", "audioIsUnavailable": "Lyd er ikke tilgængelig for denne strøm", "audio": { @@ -67,7 +67,10 @@ "desc": "Aktivér kun denne mulighed, hvis kameraets live stream viser farve artefakter og har en diagonal linje på højre side af billedet." } } - } + }, + "showAll": "Vis alle kameragrupper", + "showLess": "Vis mindre", + "editGroups": "Rediger kameragrupper" }, "debug": { "options": { @@ -81,6 +84,7 @@ "zones": "Zoner", "mask": "Maske", "motion": "Bevægelse", - "regions": "Regioner" + "regions": "Regioner", + "paths": "Stier" } } diff --git a/web/public/locales/da/components/dialog.json b/web/public/locales/da/components/dialog.json index a498a33f54..1ab0861830 100644 --- a/web/public/locales/da/components/dialog.json +++ b/web/public/locales/da/components/dialog.json @@ -1,6 +1,6 @@ { "restart": { - "title": "Er du sikker på at du vil genstarte Frigate?", + "title": "Er du sikker på, at du vil genstarte Frigate?", "button": "Genstart", "restarting": { "title": "Frigate genstarter", @@ -21,8 +21,46 @@ "ask_a": "Er dette objekt et {{label}}?", "ask_an": "Er dette objekt en {{label}}?", "ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "state": { + "submitted": "Indsendt" + }, + "toast": { + "error": "Fejl under indsendelse til Frigate+. Kontroller venligst din netværksforbindelse og prøv igen." } } + }, + "video": { + "viewInHistory": "Se i historik" } + }, + "export": { + "time": { + "fromTimeline": "Vælg fra tidslinje", + "lastHour_one": "Sidste time", + "lastHour_other": "Sidste {{count}} timer", + "custom": "Tidsinterval", + "start": { + "title": "Starttidspunkt", + "label": "Vælg starttidspunkt" + }, + "end": { + "title": "Sluttidspunkt", + "label": "Vælg sluttidspunkt" + } + }, + "name": { + "placeholder": "Navngiv eksport" + }, + "case": { + "newCaseOption": "Opret ny sag", + "newCaseNamePlaceholder": "Nyt sagsnavn", + "newCaseDescriptionPlaceholder": "Sagsbeskrivelse", + "label": "Sag", + "nonAdminHelp": "En ny sag oprettes til disse eksporter.", + "placeholder": "Vælg en sag" + }, + "select": "Vælg", + "export": "Eksporter" } } diff --git a/web/public/locales/da/views/settings.json b/web/public/locales/da/views/settings.json index 7b5d669ed4..816d0d60e9 100644 --- a/web/public/locales/da/views/settings.json +++ b/web/public/locales/da/views/settings.json @@ -14,6 +14,7 @@ "notifications": "Notifikations indstillinger - Frigate" }, "menu": { - "ui": "Brugergrænseflade" + "ui": "Brugergrænseflade", + "profiles": "Profiler" } } diff --git a/web/public/locales/de/audio.json b/web/public/locales/de/audio.json index 78f4eabb3b..045beee914 100644 --- a/web/public/locales/de/audio.json +++ b/web/public/locales/de/audio.json @@ -23,7 +23,7 @@ "toothbrush": "Zahnbürste", "bicycle": "Fahrrad", "door": "Tür", - "keyboard": "Klaviatur", + "keyboard": "Klavier", "bus": "Bus", "horse": "Pferd", "cat": "Katze", @@ -123,7 +123,7 @@ "chicken": "Huhn", "sitar": "Sitar", "ukulele": "Ukulele", - "tapping": "Klopfen", + "tapping": "Tippen", "flapping_wings": "Flügelschlagen", "strum": "Herumklimpern", "electronic_organ": "Elektrische Orgel", diff --git a/web/public/locales/de/common.json b/web/public/locales/de/common.json index 00d9b8b60b..1ddb0488ba 100644 --- a/web/public/locales/de/common.json +++ b/web/public/locales/de/common.json @@ -42,7 +42,7 @@ "untilRestart": "Bis zum Neustart", "justNow": "Gerade", "pm": "nachmittags", - "mo": "{{time}} Mon", + "mo": "{{time}} Mon.", "formattedTimestamp": { "12hour": "d. MMM, hh:mm:ss aaa", "24hour": "dd. MMM, hh:mm:ss aaa" @@ -193,7 +193,7 @@ "gl": "Galego (Galicisch)", "id": "Bahasa Indonesia (Indonesisch)", "hr": "Hrvatski (Kroatisch)", - "bs": "Bosnisch", + "bs": "Bosanski (Bosnisch)", "zhHant": "Traditional Chinese" }, "appearance": "Erscheinung", diff --git a/web/public/locales/de/components/camera.json b/web/public/locales/de/components/camera.json index a2b443bd66..ff98559e66 100644 --- a/web/public/locales/de/components/camera.json +++ b/web/public/locales/de/components/camera.json @@ -68,7 +68,10 @@ }, "label": "Kamera Gruppen", "edit": "Kameragruppe bearbeiten", - "success": "Kameragruppe {{name}} wurde gespeichert." + "success": "Kameragruppe {{name}} wurde gespeichert.", + "showAll": "Alle Kameragruppen anzeigen", + "showLess": "Weniger anzeigen", + "editGroups": "Kameragruppen bearbeiten" }, "debug": { "options": { diff --git a/web/public/locales/de/components/dialog.json b/web/public/locales/de/components/dialog.json index 59dac7aeda..fe0db22dc8 100644 --- a/web/public/locales/de/components/dialog.json +++ b/web/public/locales/de/components/dialog.json @@ -105,14 +105,21 @@ "cameraSelection": "Kameras", "cameraSelectionHelp": "Kameras, die in diesem Zeitbereich Objekte verfolgen, sind vorausgewählt", "checkingActivity": "Kameraaktivität wird überprüft...", - "noCameras": "keine kamaeras verfügbar", + "noCameras": "Keine Kameras verfügbar", "detectionCount_one": "1 verfolgtes Objekt", - "detectionCount_other": "{{count}} verfolgtesObjekte", - "nameLabel": "Export Name", + "detectionCount_other": "{{count}} verfolgte Objekte", + "nameLabel": "Exportname", "namePlaceholder": "Optionaler Basisname für diese Exporte", "queueingButton": "Exporte werden in die Warteschlange gestellt...", "exportButton_one": "Export 1 Kamera", - "exportButton_other": "xport {{count}} Kameras" + "exportButton_other": "xport {{count}} Kameras", + "searchOrSelectGroup": "Suchen, oder Kameragruppe auswählen...", + "selectAll": "Alle Kameras auswählen", + "clearSelection": "Auswahl löschen", + "selectWithActivity": "Kameras mit verfolgten Objekten", + "selectGroup": "Gruppe auswählen", + "noMatchingCameras": "Der Suche entsprechen keine Kameras", + "selectedCount": "{{selected}} / {{total}} ausgewählt" }, "multi": { "title_one": "1 Bewertung exportieren", diff --git a/web/public/locales/de/config/cameras.json b/web/public/locales/de/config/cameras.json index 4a479c2c23..7865c4957c 100644 --- a/web/public/locales/de/config/cameras.json +++ b/web/public/locales/de/config/cameras.json @@ -20,7 +20,7 @@ "description": "Mindest-RMS-Lautstärkeschwelle, die für die Audioerkennung erforderlich ist; niedrigere Werte erhöhen die Empfindlichkeit (z. B. 200 hoch, 500 mittel, 1000 niedrig)." }, "listen": { - "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, schreien, sprechen, rufen).", + "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, Gespräche, Rufen).", "label": "Hörtypen" }, "filters": { @@ -204,11 +204,11 @@ "description": "Einstellungen zum Aktivieren und Verwalten von Benachrichtigungen für diese Kamera." }, "ffmpeg": { - "label": "FFmpeg", - "description": "FFmpeg-Einstellungen, einschließlich Binärpfad, Argumente, hwaccel-Optionen und rollenspezifische Ausgabeargumente.", + "label": "Streams (FFmpeg)", + "description": "Kamera-Stream-Eingaben und FFmpeg-Optionen, einschließlich Binärpfad, Argumente, hwaccel und rollenspezifische Ausgabeargumente.", "path": { "label": "FFmpeg-Pfad", - "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („5.0” oder „7.0”)." + "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)." }, "global_args": { "label": "Globale Argumente von FFmpeg", @@ -770,6 +770,10 @@ "dashboard": { "label": "In der Benutzeroberfläche anzeigen", "description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen." + }, + "review": { + "label": "In der Überprüfung anzeigen", + "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)." } }, "snapshots": { diff --git a/web/public/locales/de/config/global.json b/web/public/locales/de/config/global.json index 78712b1035..785a503bc4 100644 --- a/web/public/locales/de/config/global.json +++ b/web/public/locales/de/config/global.json @@ -18,7 +18,7 @@ "description": "Mindest-RMS-Lautstärkeschwelle, die für die Audioerkennung erforderlich ist; niedrigere Werte erhöhen die Empfindlichkeit (z. B. 200 hoch, 500 mittel, 1000 niedrig)." }, "listen": { - "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, schreien, sprechen, rufen).", + "description": "Liste der zu erkennenden Audioereignisse (z.B: bellen, Feueralarm, Gespräche, Rufen).", "label": "Hörtypen" }, "filters": { @@ -380,7 +380,7 @@ "description": "FFmpeg-Einstellungen, einschließlich Binärpfad, Argumente, hwaccel-Optionen und rollenspezifische Ausgabeargumente.", "path": { "label": "FFmpeg-Pfad", - "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („5.0” oder „7.0”)." + "description": "Pfad zur zu verwendenden FFmpeg-Binärdatei oder ein Versionsalias („7.0” oder „8.0”)." }, "global_args": { "label": "Globale Argumente von FFmpeg", @@ -1934,6 +1934,10 @@ "dashboard": { "label": "In der Benutzeroberfläche anzeigen", "description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen." + }, + "review": { + "label": "In der Überprüfung anzeigen", + "description": "Legen Sie fest, ob diese Kamera in der Übersicht angezeigt wird (auf der Übersichtsseite sowie im Kamerafilter, in der Bewegungsübersicht und in der Verlaufsansicht)." } } } diff --git a/web/public/locales/de/objects.json b/web/public/locales/de/objects.json index 4380ef181e..e2236fc5c8 100644 --- a/web/public/locales/de/objects.json +++ b/web/public/locales/de/objects.json @@ -90,7 +90,7 @@ "laptop": "Laptop", "mouse": "Maus", "goat": "Ziege", - "keyboard": "Klaviatur", + "keyboard": "Klavier", "cell_phone": "Handy", "remote": "Fernbedienung", "airplane": "Flugzeug", diff --git a/web/public/locales/de/views/motionSearch.json b/web/public/locales/de/views/motionSearch.json index 9ad72c4616..d6aaf98fb1 100644 --- a/web/public/locales/de/views/motionSearch.json +++ b/web/public/locales/de/views/motionSearch.json @@ -46,7 +46,7 @@ "threshold": "Empfindlichkeitsschwelle", "thresholdDesc": "Niedrigere Werte erkennen geringere Veränderungen (1–255)", "minArea": "Mindestwechselbereich", - "minAreaDesc": "Mindestanteil der untersuchten Region, der sich ändern muss, damit die Veränderung als signifikant gilt", + "minAreaDesc": "Mindestgröße eines einzelnen sich bewegenden Bereichs, ausgedrückt als Prozentsatz des untersuchten Bereichs", "frameSkip": "Bild überspringen", "frameSkipDesc": "Verarbeite jeden N-ten Frame. Stelle diesen Wert auf die Bildrate deiner Kamera ein, um einen Frame pro Sekunde zu verarbeiten (z. B. 5 für eine Kamera mit 5 FPS, 30 für eine Kamera mit 30 FPS). Höhere Werte sorgen für eine schnellere Verarbeitung, können jedoch kurze Bewegungsabläufe übersehen.", "maxResults": "Maximale Ergebnisse", @@ -72,6 +72,9 @@ "framesDecoded": "Rahmen decodiert", "wallTime": "Suchzeit", "segmentErrors": "Segmentfehler", - "seconds": "{{seconds}}s" - } + "seconds": "{{seconds}}s", + "minutesSeconds": "{{minutes}}m {{seconds}}s", + "scanSummary": "{{segments}} Segmente · {{time}}" + }, + "scanning": "Wird gescannt {{time}}" } diff --git a/web/public/locales/de/views/settings.json b/web/public/locales/de/views/settings.json index 3d8e9d3b83..a0fcfd731f 100644 --- a/web/public/locales/de/views/settings.json +++ b/web/public/locales/de/views/settings.json @@ -70,7 +70,7 @@ "integrationObjectClassification": "Objekt Klassifizierung", "integrationAudioTranscription": "Audio-Transkription", "cameraDetect": "Objekterkennung", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Streams (FFmpeg)", "cameraRecording": "Aufnahme", "cameraSnapshots": "Momentaufnahme", "cameraMotion": "Bewegungserkennung", @@ -1376,12 +1376,16 @@ "details": { "edit": "Kameradaten bearbeiten", "title": "Kameradaten bearbeiten", - "description": "Aktualisieren Sie den Anzeigenamen und die externe URL, die für diese Kamera in der gesamten Frigate-Benutzeroberfläche verwendet werden.", + "description": "Aktualisieren Sie den Anzeigenamen, die externe URL und die Sichtbarkeit, die für diese Kamera in der gesamten Frigate-Benutzeroberfläche verwendet werden.", "friendlyNameLabel": "Display Name", "friendlyNameHelp": "Der in der Benutzeroberfläche von „Frigate“ für diese Kamera angezeigte Spitzname. Lassen Sie das Feld leer, um die Kamera-ID zu verwenden.", "webuiUrlLabel": "URL der Web-Benutzeroberfläche", "webuiUrlHelp": "URL, um die Web-Benutzeroberfläche der Kamera direkt aus der Debug-Ansicht aufzurufen. Lassen Sie das Feld leer, um den Link zu deaktivieren.", - "webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com)." + "webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com).", + "dashboardLabel": "Im Live-Dashboard anzeigen", + "dashboardHelp": "Diese Kamera im Live-Dashboard anzeigen.", + "reviewLabel": "In der Überprüfung anzeigen", + "reviewHelp": "Zeige diese Kamera in der Übersicht an, einschließlich des Kamerafilters, der Bewegungsübersicht und der Verlaufsansicht." }, "label": "Kamerazustand", "description": "Legen Sie den Betriebszustand für jede Kamera fest.

Ein: Streams werden normal verarbeitet.
Aus: Die Verarbeitung wird vorübergehend angehalten. Diese Einstellung bleibt bei einem Neustart von Frigate nicht erhalten.
Deaktiviert: Die Verarbeitung wird beendet und die Änderung in Ihrer Konfiguration gespeichert. Um eine deaktivierte Kamera wieder zu aktivieren, ist ein Neustart erforderlich.

Hinweis: Die Deaktivierung hat keine Auswirkungen auf go2rtc-Restreams.

Ziehen Sie den Griff, um die Reihenfolge der aktiven Kameras in der Benutzeroberfläche anzupassen, einschließlich des Live-Dashboards und der Dropdown-Menüs zur Kameraauswahl.", @@ -1764,7 +1768,17 @@ } }, "cameraInputs": { - "itemTitle": "Stream {{index}}" + "itemTitle": "Stream {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Pfad für die manuelle Eingabe", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Wählen Sie einen go2rtc-Stream aus", + "noGo2rtcStreams": "Es sind keine go2rtc-Streams konfiguriert", + "go2rtcStreamSearch": "Suche Streams...", + "availableStreams": "Verfügbare Streams", + "noMatchingStreams": "Keine passenden Streams" + } }, "restartRequiredField": "Neustart erforderlich", "restartRequiredFooter": "Konfiguration geändert – Neustart erforderlich", @@ -2082,7 +2096,11 @@ "fpsGreaterThanFive": "Es wird nicht empfohlen, den Wert für die FPS-Erkennung auf mehr als 5 zu setzen. Höhere Werte können zu Leistungseinbußen führen und bieten keinerlei Vorteile.", "disabled": "Die Objekterkennung ist deaktiviert. Momentaufnahmen, Überprüfungselemente und Erweiterungsfunktionen wie Gesichtserkennung, Kennzeichenerkennung und generative KI funktionieren nicht.", "resolutionShouldBeMultipleOfFour": "Um optimale Ergebnisse zu erzielen, sollten Breite und Höhe ein Vielfaches von 4 sein. Andere gerade Werte können zu visuellen Artefakten oder leichten Verzerrungen im Erkennungsstrom führen.", - "aspectRatioMismatch": "Die von Ihnen eingegebene Breite und Höhe stimmen nicht mit dem Seitenverhältnis Ihrer aktuell erkannten Auflösung überein. Dies kann zu einem gestreckten oder verzerrten Bild führen." + "aspectRatioMismatch": "Die von Ihnen eingegebene Breite und Höhe stimmen nicht mit dem Seitenverhältnis Ihrer aktuell erkannten Auflösung überein. Dies kann zu einem gestreckten oder verzerrten Bild führen.", + "maxFramesSet": "Die Festlegung einer maximalen Bildrate überschreibt das Standardverhalten und deaktiviert die Verfolgung stationärer Objekte. Dies ist nur in sehr wenigen Fällen erforderlich; verwenden Sie diese Option daher mit Bedacht.", + "squareResolution": "Eine quadratische Erkennungsauflösung ist ungewöhnlich. Die Erkennungsbreite und -höhe sollten dem Seitenverhältnis Ihrer Kamera entsprechen (zum Beispiel 16:9) und nicht den Abmessungen des Objekterkennungsmodells. Ein nicht übereinstimmendes Seitenverhältnis kann das Bild verzerren und die Erkennungsgenauigkeit beeinträchtigen.", + "resolutionHigh": "Diese Erkennungsauflösung liegt über der empfohlenen Wert und kann zu einem erhöhten Ressourcenverbrauch führen, ohne die Erkennungsgenauigkeit zu verbessern. Für die meisten Kameras wird eine Erkennungsauflösung von maximal 1080p empfohlen.", + "globalResolutionMultipleCameras": "Bei der Konfiguration mehrerer Kameras wird eine globale Erkennungsauflösung festgelegt. Sofern nicht alle Kameras dieselbe Auflösung und dasselbe Seitenverhältnis aufweisen, sollten die Erkennungsbreite und -höhe für jede Kamera separat festgelegt werden, um dem nativen Seitenverhältnis der jeweiligen Kamera zu entsprechen." }, "faceRecognition": { "globalDisabled": "Die Gesichtserkennungserweiterung muss aktiviert sein, damit die Gesichtserkennungsfunktionen bei dieser Kamera funktionieren.", @@ -2115,6 +2133,13 @@ }, "onvif": { "autotrackingNoZones": "Für die automatische Verfolgung ist mindestens eine Zone erforderlich. Definieren Sie unter „Masken / Zonen“ eine Zone für diese Kamera und legen Sie diese anschließend unten als erforderliche Zone fest." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Explizite Definitionen der Hardware-beschleunigungs Variablen sind nicht empfohlen. Wähle die Voreinstellung die zu deiner Hardware passt, außer wenn spezifische Anforderungen eine andere Konfiguration erfordern." + }, + "model": { + "optimizedFor320": "Frigate ist für ein 320x320-Modell optimiert, was für die meisten Konfigurationen die beste Wahl ist. Ein 640x640-Modell ist langsamer und bietet nur in bestimmten Szenarien Vorteile.", + "inputDimensionsNotDetectResolution": "Die Eingangsbreite und -höhe des Modells beziehen sich auf die Abmessungen des Objekterkennungsmodells und nicht auf die Erkennungsauflösung Ihrer Kamera. Sie sollten mit den Abmessungen des von Ihnen verwendeten Modells übereinstimmen – in der Regel quadratische Abmessungen wie 320×320 oder 640×640." } }, "birdseye": { diff --git a/web/public/locales/de/views/system.json b/web/public/locales/de/views/system.json index 20d5cc1fa4..567c6ae68e 100644 --- a/web/public/locales/de/views/system.json +++ b/web/public/locales/de/views/system.json @@ -173,7 +173,22 @@ "tips": { "title": "Kamera-Untersuchsungsinfo" }, - "aspectRatio": "Seitenverhältnis" + "aspectRatio": "Seitenverhältnis", + "keyframes": { + "title": "Keyframe-Analyse", + "analyzing": "Keyframes werden analysiert... Noch {{seconds}} Sekunden", + "stillAnalyzing": "Keyframes werden noch analysiert...", + "recordStream": "Stream aufzeichnen:", + "keyframeCount": "Beobachtete Keyframes:", + "observedDuration": "Beobachtete Dauer:", + "gap": "Keyframe-Abstand (min. / durchschnittlich / max.):", + "segmentLength": "Länge des Aufzeichnungssegments:", + "ok": "Keyframes alle ~{{seconds}}s, gut geeignet für Aufzeichnung und Wiedergabe.", + "warning": "Seltene oder unregelmäßige Keyframes (längste Lücke ~{{seconds}}s), wahrscheinlich ein „Smart“-Codec (H.264+/H.265+); dies wird nicht empfohlen.", + "error": "Die Lücke zwischen den Keyframes (~{{seconds}}s) überschreitet die Länge des Aufzeichnungssegments ({{segmentTime}}s). Einige Segmente enthalten möglicherweise keinen Keyframe, was zu einer Unterbrechung der Wiedergabe führt. Deaktivieren Sie den Smart/+-Codec an der Kamera oder verkürzen Sie dessen Keyframe-Intervall.", + "unknown": "Der Abstand zwischen den Keyframes konnte nicht ermittelt werden.", + "recordDisabled": "Die Aufzeichnung ist für diese Kamera deaktiviert." + } }, "overview": "Übersicht", "label": { diff --git a/web/public/locales/el/audio.json b/web/public/locales/el/audio.json index 2bd01b8717..f3344f1186 100644 --- a/web/public/locales/el/audio.json +++ b/web/public/locales/el/audio.json @@ -61,5 +61,14 @@ "yodeling": "Λαρυγγισμός", "chant": "Ύμνος", "mantra": "Μάντρα", - "synthetic_singing": "Συνθετικό Τραγούδι" + "synthetic_singing": "Συνθετικό Τραγούδι", + "rapping": "Χτύπημα", + "humming": "Μουρμουρητό", + "groan": "Aναστεναγμός", + "grunt": "Μούγκρισμα", + "gasp": "Ανάσα", + "pant": "Λαχάνιασμα", + "snort": "Ρουθούνισμα", + "cough": "Βήχας", + "throat_clearing": "Ξεροβήξιμο" } diff --git a/web/public/locales/el/common.json b/web/public/locales/el/common.json index 5978d6cff6..256fcfb8aa 100644 --- a/web/public/locales/el/common.json +++ b/web/public/locales/el/common.json @@ -109,7 +109,9 @@ "no": "Όχι", "download": "Κατέβασμα", "info": "Πληροφορίες", - "history": "Ιστορία" + "history": "Ιστορία", + "on": "Ενεργοποίηση", + "off": "Απενεργοποίηση" }, "unit": { "speed": { @@ -117,7 +119,8 @@ "kph": "χλμ/ώρα" }, "length": { - "meters": "μέτρα" + "meters": "μέτρα", + "feet": "πόδια" }, "data": { "kbps": "kB/s", @@ -134,7 +137,8 @@ "show": "Εμφάνιση {{item}}", "ID": "ID", "none": "Κανένα", - "all": "Όλα" + "all": "Όλα", + "other": "άλλο" }, "toast": { "save": { diff --git a/web/public/locales/en/components/dialog.json b/web/public/locales/en/components/dialog.json index 3630d68e09..dcb4a23db4 100644 --- a/web/public/locales/en/components/dialog.json +++ b/web/public/locales/en/components/dialog.json @@ -24,6 +24,9 @@ }, "state": { "submitted": "Submitted" + }, + "toast": { + "error": "Failed to submit to Frigate+. Please check your network connection and try again." } } }, @@ -70,6 +73,13 @@ "selectFromTimeline": "Select from Timeline", "cameraSelection": "Cameras", "cameraSelectionHelp": "Cameras with tracked objects in this time range are pre-selected", + "searchOrSelectGroup": "Search, or select a camera group...", + "selectAll": "Select all cameras", + "clearSelection": "Clear selection", + "selectWithActivity": "Cameras with tracked objects", + "selectGroup": "Select group", + "noMatchingCameras": "No cameras match your search", + "selectedCount": "{{selected}} / {{total}} selected", "checkingActivity": "Checking camera activity...", "noCameras": "No cameras available", "detectionCount_one": "1 tracked object", @@ -113,7 +123,7 @@ "error": { "failed": "Failed to queue export: {{error}}", "endTimeMustAfterStartTime": "End time must be after start time", - "noVaildTimeSelected": "No valid time range selected" + "noValidTimeSelected": "No valid time range selected" } }, "fromTimeline": { diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index 98e625abf7..2ddc30b6b8 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -29,7 +29,7 @@ }, "listen": { "label": "Listen types", - "description": "List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell)." + "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)." }, "filters": { "label": "Audio filters", @@ -152,11 +152,11 @@ } }, "ffmpeg": { - "label": "FFmpeg", - "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", + "label": "Streams (FFmpeg)", + "description": "Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.", "path": { "label": "FFmpeg path", - "description": "Path to the FFmpeg binary to use or a version alias (\"5.0\" or \"7.0\")." + "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")." }, "global_args": { "label": "FFmpeg global arguments", @@ -493,6 +493,9 @@ "max_concurrent": { "label": "Maximum concurrent exports", "description": "Maximum number of export jobs to process at the same time." + }, + "chapters": { + "label": "Chapter metadata to embed in exported recordings" } }, "preview": { @@ -666,10 +669,6 @@ "label": "Default retention", "description": "Default number of days to retain snapshots." }, - "mode": { - "label": "Retention mode", - "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." - }, "objects": { "label": "Object retention", "description": "Per-object overrides for snapshot retention days." diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index 119d384e43..e3b7c65c68 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -547,7 +547,7 @@ }, "listen": { "label": "Listen types", - "description": "List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell)." + "description": "List of audio event types to detect (for example: bark, fire_alarm, speech, yell)." }, "filters": { "label": "Audio filters", @@ -683,7 +683,7 @@ "description": "FFmpeg settings including binary path, args, hwaccel options, and per-role output args.", "path": { "label": "FFmpeg path", - "description": "Path to the FFmpeg binary to use or a version alias (\"5.0\" or \"7.0\")." + "description": "Path to the FFmpeg binary to use or a version alias (\"7.0\" or \"8.0\")." }, "global_args": { "label": "FFmpeg global arguments", @@ -1035,6 +1035,9 @@ "max_concurrent": { "label": "Maximum concurrent exports", "description": "Maximum number of export jobs to process at the same time." + }, + "chapters": { + "label": "Chapter metadata to embed in exported recordings" } }, "preview": { @@ -1176,10 +1179,6 @@ "label": "Default retention", "description": "Default number of days to retain snapshots." }, - "mode": { - "label": "Retention mode", - "description": "Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects)." - }, "objects": { "label": "Object retention", "description": "Per-object overrides for snapshot retention days." diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index 3206ad0339..3e881ae345 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -1,5 +1,6 @@ { "documentTitle": "Classification Models - Frigate", + "disabled": "Disabled", "details": { "scoreInfo": "Score represents the average classification confidence across all detections of this object.", "none": "None", @@ -64,7 +65,20 @@ "title": "Edit Classification Model", "descriptionState": "Edit the classes for this state classification model. Changes will require retraining the model.", "descriptionObject": "Edit the object type and classification type for this object classification model.", - "stateClassesInfo": "Note: Changing state classes requires retraining the model with the updated classes." + "enabled": "Enabled", + "enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.", + "saveAttempts": "Save Attempts", + "saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.", + "motion": "Run on Motion", + "motionDesc": "Run classification when motion is detected within the configured crop.", + "interval": "Interval", + "intervalDesc": "Seconds between periodic classification runs. Leave empty to run only on motion.", + "intervalPlaceholder": "No interval", + "stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.", + "errors": { + "saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater", + "intervalInvalid": "Interval must be a whole number greater than 0" + } }, "deleteDatasetImages": { "title": "Delete Dataset Images", diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index d1087b3c96..c85ee90d53 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -76,7 +76,7 @@ }, "offset": { "label": "Annotation Offset", - "desc": "This data comes from your camera's detect feed but is overlayed on images from the the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", + "desc": "This data comes from your camera's detect feed but is overlaid on images from the record feed. It is unlikely that the two streams are perfectly in sync. As a result, the bounding box and the footage will not line up perfectly. You can use this setting to offset the annotations forward or backward in time to better align them with the recorded footage.", "millisecondsToOffset": "Milliseconds to offset detect annotations by. Default: 0", "tips": "Lower the value if the video playback is ahead of the boxes and path points, and increase the value if the video playback is behind them. This value can be negative.", "toast": { diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 5dbb01b33b..8efde7a61e 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -85,7 +85,7 @@ "integrationObjectClassification": "Object classification", "integrationAudioTranscription": "Audio transcription", "cameraDetect": "Object detection", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Streams (FFmpeg)", "cameraRecording": "Recording", "cameraSnapshots": "Snapshots", "cameraMotion": "Motion detection", @@ -1053,7 +1053,7 @@ }, "createUser": { "title": "Create New User", - "desc": "Add a new user account and specify an role for access to areas of the Frigate UI.", + "desc": "Add a new user account and specify a role for access to areas of the Frigate UI.", "usernameOnlyInclude": "Username may only include letters, numbers, . or _", "confirmPassword": "Please confirm your password" }, @@ -1497,7 +1497,14 @@ "keyLabel": "Key", "valueLabel": "Value", "keyPlaceholder": "New key", - "remove": "Remove" + "remove": "Remove", + "providerNameLabel": "Provider name", + "providerNamePlaceholder": "e.g., openai", + "variableNameLabel": "Variable name", + "variableNamePlaceholder": "e.g., MY_VARIABLE", + "loggerNameLabel": "Logger name", + "loggerNamePlaceholder": "e.g., frigate.record", + "keyPatternError": "Use only letters, numbers, hyphens, and underscores (no spaces)" }, "knownPlates": { "namePlaceholder": "e.g., Wife's Car", @@ -1560,7 +1567,17 @@ } }, "cameraInputs": { - "itemTitle": "Stream {{index}}" + "itemTitle": "Stream {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Manual input path", + "go2rtcStreamLabel": "go2rtc stream", + "go2rtcStreamPlaceholder": "Select a go2rtc stream", + "noGo2rtcStreams": "No go2rtc streams configured", + "go2rtcStreamSearch": "Search streams...", + "availableStreams": "Available streams", + "noMatchingStreams": "No matching streams" + } }, "restartRequiredField": "Restart required", "restartRequiredFooter": "Configuration changed - Restart required", @@ -1921,6 +1938,13 @@ "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." + }, "objects": { "genaiNoDescriptionsProvider": "You must configure a GenAI provider with the 'descriptions' role for descriptions to be generated." }, diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index b824e0749c..84014aaffe 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -107,12 +107,7 @@ }, "npuUsage": "NPU Usage", "npuMemory": "NPU Memory", - "npuTemperature": "NPU Temperature", - "intelGpuWarning": { - "title": "Intel GPU Stats Warning", - "message": "GPU stats unavailable", - "description": "This is a known bug in Intel's GPU stats reporting tools (intel_gpu_top) where it will break and repeatedly return a GPU usage of 0% even in cases where hardware acceleration and object detection are correctly running on the (i)GPU. This is not a Frigate bug. You can restart the host to temporarily fix the issue and confirm that the GPU is working correctly. This does not affect performance." - } + "npuTemperature": "NPU Temperature" }, "otherProcesses": { "title": "Other Processes", @@ -174,6 +169,21 @@ "error": "Error: {{error}}", "tips": { "title": "Camera Probe Info" + }, + "keyframes": { + "title": "Keyframe analysis", + "analyzing": "Analyzing keyframes... {{seconds}} seconds remaining", + "stillAnalyzing": "Still analyzing keyframes...", + "recordStream": "Record stream:", + "keyframeCount": "Keyframes observed:", + "observedDuration": "Observed duration:", + "gap": "Keyframe gap (min / avg / max):", + "segmentLength": "Recording segment length:", + "ok": "Keyframes every ~{{seconds}}s, good for recording and playback.", + "warning": "Sparse or variable keyframes (longest gap ~{{seconds}}s), likely a smart codec (H.264+/H.265+), this is not recommended.", + "error": "Keyframe gap (~{{seconds}}s) exceeds the recording segment length ({{segmentTime}}s). Some segments may have no keyframe, which breaks playback. Disable the smart/+ codec on the camera or shorten its keyframe interval.", + "unknown": "Couldn't determine keyframe spacing.", + "recordDisabled": "Recording is disabled for this camera." } }, "framesAndDetections": "Frames / Detections", diff --git a/web/public/locales/es/components/dialog.json b/web/public/locales/es/components/dialog.json index c52e427bb6..d86eb5036a 100644 --- a/web/public/locales/es/components/dialog.json +++ b/web/public/locales/es/components/dialog.json @@ -119,7 +119,14 @@ "queueingButton": "Poniendo exportaciones en cola...", "exportButton_one": "Exportar 1 cámara", "exportButton_many": "Exportar {{count}} cámaras", - "exportButton_other": "Exportar {{count}} cámaras" + "exportButton_other": "Exportar {{count}} cámaras", + "searchOrSelectGroup": "Busca o selecciona un grupo de cámaras…", + "selectAll": "Seleccionar todas las cámaras", + "clearSelection": "Borrar selección", + "selectWithActivity": "Cámaras con objetos rastreados", + "selectGroup": "Seleccionar grupo", + "noMatchingCameras": "Ninguna cámara coincide con tu búsqueda", + "selectedCount": "{{selected}} / {{total}} seleccionadas" }, "multi": { "title_one": "Exportar 1 revisión", diff --git a/web/public/locales/es/config/cameras.json b/web/public/locales/es/config/cameras.json index aa1f09becc..95e412ff4e 100644 --- a/web/public/locales/es/config/cameras.json +++ b/web/public/locales/es/config/cameras.json @@ -24,7 +24,7 @@ }, "listen": { "label": "Tipos de escucha", - "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrido, alarma de incendios, grito, voz, alarido)." + "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)." }, "filters": { "label": "Filtros de audio", @@ -744,7 +744,7 @@ "label": "Tiempo de reintento de FFmpeg" }, "path": { - "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\").", + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").", "label": "Ruta de FFmpeg" }, "output_args": { @@ -759,8 +759,8 @@ "description": "Argumentos de salida predeterminados para los flujos con rol de grabación." } }, - "description": "Configuración de FFmpeg, incluyendo la ruta del binario, argumentos, opciones de aceleración por hardware y argumentos de salida por rol.", - "label": "FFmpeg", + "description": "Entradas de flujo de la cámara y opciones de FFmpeg, incluida la ruta del binario, argumentos, aceleración por hardware y argumentos de salida por rol.", + "label": "Flujos (FFmpeg)", "global_args": { "label": "Argumentos globales de FFmpeg", "description": "Argumentos globales pasados a los procesos de FFmpeg." diff --git a/web/public/locales/es/config/global.json b/web/public/locales/es/config/global.json index ee7210f359..7869ce4ba2 100644 --- a/web/public/locales/es/config/global.json +++ b/web/public/locales/es/config/global.json @@ -39,7 +39,7 @@ }, "listen": { "label": "Tipos de escucha", - "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrido, alarma de incendios, grito, voz, alarido)." + "description": "Lista de tipos de eventos de audio a detectar (por ejemplo: ladrar, alarma de incendio, habla, gritar)." }, "filters": { "label": "Filtros de audio", @@ -1234,7 +1234,7 @@ "label": "Tiempo de reintento de FFmpeg" }, "path": { - "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"5.0\" o \"7.0\").", + "description": "Ruta al binario de FFmpeg que se va a utilizar o un alias de versión (\"7.0\" o \"8.0\").", "label": "Ruta de FFmpeg" }, "output_args": { diff --git a/web/public/locales/es/views/settings.json b/web/public/locales/es/views/settings.json index 99aa74e98d..f5e98ebec3 100644 --- a/web/public/locales/es/views/settings.json +++ b/web/public/locales/es/views/settings.json @@ -72,7 +72,7 @@ "integrationObjectClassification": "Clasificación de objetos", "integrationAudioTranscription": "Transcripción de audio", "cameraDetect": "Detección de objetos", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Flujos (FFmpeg)", "cameraRecording": "Grabación", "cameraSnapshots": "Instantáneas", "cameraMotion": "Detección de movimiento", @@ -1720,7 +1720,17 @@ } }, "cameraInputs": { - "itemTitle": "Flujo {{index}}" + "itemTitle": "Flujo {{index}}", + "sourceMode": { + "restream": "Retransmisión (go2rtc)", + "manual": "Ruta de entrada manual", + "go2rtcStreamLabel": "Flujo go2rtc", + "go2rtcStreamPlaceholder": "Selecciona un flujo go2rtc", + "noGo2rtcStreams": "No hay flujos go2rtc configurados", + "go2rtcStreamSearch": "Buscar flujos…", + "availableStreams": "Flujos disponibles", + "noMatchingStreams": "No hay flujos coincidentes" + } }, "restartRequiredField": "Reinicio necesario", "restartRequiredFooter": "Configuración modificada - reinicio necesario", @@ -2020,6 +2030,9 @@ }, "onvif": { "autotrackingNoZones": "El seguimiento automático requiere al menos una zona. Define una zona para esta cámara en Máscaras / Zonas y, a continuación, establécela como zona obligatoria a continuación." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "No son remontados los argumentos de aceleración por hardware manual. A no ser que un requisito específico exista, seleccione la preselección que coincida con su hardware." } }, "resetToDefaultDescription": "Esto restablecerá todos los ajustes de esta sección a sus valores predeterminados. Esta acción no se puede deshacer.", diff --git a/web/public/locales/es/views/system.json b/web/public/locales/es/views/system.json index 23ee553a12..09f26febd4 100644 --- a/web/public/locales/es/views/system.json +++ b/web/public/locales/es/views/system.json @@ -81,16 +81,16 @@ "gpuInfo": { "vainfoOutput": { "title": "Salida de Vainfo", - "returnCode": "Código de Retorno: {{code}}", - "processOutput": "Salida del Proceso:", - "processError": "Error del Proceso:" + "returnCode": "Código de retorno: {{code}}", + "processOutput": "Salida del proceso:", + "processError": "Error del proceso:" }, "nvidiaSMIOutput": { - "cudaComputerCapability": "Capacidad de Cómputo CUDA: {{cuda_compute}}", + "cudaComputerCapability": "Capacidad de cómputo CUDA: {{cuda_compute}}", "title": "Salida de Nvidia SMI", "driver": "Controlador: {{driver}}", "name": "Nombre: {{name}}", - "vbios": "Información de VBios: {{vbios}}" + "vbios": "Informe de VBios: {{vbios}}" }, "toast": { "success": "Información de GPU copiada al portapapeles" @@ -104,7 +104,7 @@ }, "gpuMemory": "Memoria de GPU", "npuMemory": "Memoria de NPU", - "npuUsage": "Uso de NPU", + "npuUsage": "Modo de empleo de NPU", "intelGpuWarning": { "title": "Aviso de estadísticas Intel GPU", "message": "Estadísticas de GPU no disponibles", @@ -122,7 +122,7 @@ "go2rtc": "go2rtc", "recording": "grabación", "review_segment": "revisar segmento", - "embeddings": "embeddings", + "embeddings": "empotrados", "audio_detector": "detector de audio" } } @@ -165,8 +165,8 @@ "codec": "Codec:", "fetching": "Obteniendo Datos de la Cámara", "stream": "Flujo {{idx}}", - "video": "Video:", - "fps": "FPS:", + "video": "Vídeo:", + "fps": "CPS:", "resolution": "Resolución:", "error": "Error: {{error}}", "unknown": "Desconocido", @@ -174,7 +174,22 @@ "tips": { "title": "Información de Sondeo de la Cámara" }, - "aspectRatio": "Relación de aspecto" + "aspectRatio": "Relación de aspecto", + "keyframes": { + "title": "Análisis de clave fotograma", + "recordStream": "Flujo de grabación:", + "segmentLength": "Longitud de segmento en grabación:", + "unknown": "No pudo determinar el espaciado del fotograma.", + "analyzing": "Analizando fotogramas clave… {{seconds}} segundos restantes", + "stillAnalyzing": "Todavía analizando fotogramas clave…", + "keyframeCount": "Fotogramas clave observados:", + "observedDuration": "Duración observada:", + "gap": "Brecha de fotogramas clave (mín / med / máx):", + "recordDisabled": "La grabación está desactivada para esta cámara.", + "ok": "Cuadros cada ~{{seconds}}s, bueno para grabación y reproducción.", + "warning": "Fotograma clave escaso o variable (hueco más largo ~{{seconds}}s), probablemente un códec inteligente (H.264+/H.265+), esto no es recomendado.", + "error": "El intervalo entre fotogramas clave (~{{seconds}} s) supera la duración del segmento de grabación ({{segmentTime}} s). Es posible que algunos segmentos no contengan ningún fotograma clave, lo que impedirá su reproducción. Desactiva el códec inteligente (+) de la cámara o reduce el intervalo entre fotogramas clave." + } }, "framesAndDetections": "Fotogramas / Detecciones", "label": { @@ -204,9 +219,9 @@ }, "connectionQuality": { "excellent": "Excelente", - "poor": "Debil", - "title": "Calidad de la conexión", - "fps": "Cuadros por segundo", + "poor": "Pobre", + "title": "Calidad de Conexión", + "fps": "CPS", "expectedFps": "Cuadros por segundo esperados", "reconnectsLastHour": "Reconexiones (última hora)", "unusable": "No usable", @@ -222,25 +237,25 @@ "infPerSecond": "Inferencias Por Segundo", "embeddings": { "plate_recognition_speed": "Velocidad de Reconocimiento de Matrículas", - "face_embedding_speed": "Velocidad de Incrustación de Rostros", - "image_embedding_speed": "Velocidad de Incrustación de Imágenes", - "text_embedding_speed": "Velocidad de Incrustación de Texto", + "face_embedding_speed": "Velocidad de Empotrado Facial", + "image_embedding_speed": "Velocidad de Empotrado de Imagen", + "text_embedding_speed": "Velocidad de Empotrado de Texto", "face_recognition_speed": "Velocidad de Reconocimiento Facial", - "text_embedding": "Incrustación de Texto", + "text_embedding": "Empotrado de Texto", "face_recognition": "Reconocimiento Facial", "plate_recognition": "Reconocimiento de Matrículas", "yolov9_plate_detection": "Detección de Matrículas YOLOv9", - "image_embedding": "Incrustación de Imágenes", + "image_embedding": "Empotrado de Imagen", "yolov9_plate_detection_speed": "Velocidad de Detección de Matrículas YOLOv9", "review_description": "Revisión de descripción", - "review_description_speed": "Velocidad de revisión de la descripción", - "review_description_events_per_second": "Revisión de la descripción", + "review_description_speed": "Revisión de velocidad de descripción", + "review_description_events_per_second": "Revisión de Descripción", "object_description": "Descripción de Objeto", - "object_description_speed": "Velocidad de descripción de objeto", - "object_description_events_per_second": "Descripción de objeto", + "object_description_speed": "Velocidad de Descripción de Objeto", + "object_description_events_per_second": "Descripción de Objeto", "classification": "Clasificación de {{name}}", - "classification_speed": "Velocidad de clasificación de {{name}}", - "classification_events_per_second": "Clasificacion de eventos por segundo de {{name}}" + "classification_speed": "Velocidad de Clasificación de {{name}}", + "classification_events_per_second": "Clasificación de Eventos por Segundo de {{name}}" }, "title": "Enriquecimientos", "averageInf": "Tiempo promedio de inferencia" @@ -249,7 +264,7 @@ "ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)", "detectHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por detección ({{detectAvg}}%)", "healthy": "El sistema está saludable", - "reindexingEmbeddings": "Reindexando incrustaciones ({{processed}}% completado)", + "reindexingEmbeddings": "Reindexando empotrados ({{processed}}% completado)", "detectIsSlow": "{{detect}} es lento ({{speed}} ms)", "cameraIsOffline": "{{camera}} está desconectada", "detectIsVerySlow": "{{detect}} es muy lento ({{speed}} ms)", diff --git a/web/public/locales/fr/common.json b/web/public/locales/fr/common.json index d49150bd9d..ebec9c47f9 100644 --- a/web/public/locales/fr/common.json +++ b/web/public/locales/fr/common.json @@ -189,7 +189,8 @@ "id": "Bahasa Indonesia (Indonésien)", "ur": "اردو (Ourdou)", "hr": "Hrvatski (Croate)", - "bs": "Bosanski (Bosnien)" + "bs": "Bosanski (Bosnien)", + "zhHant": "繁體中文 (chinois traditionnel)" }, "appearance": "Apparence", "darkMode": { diff --git a/web/public/locales/fr/components/dialog.json b/web/public/locales/fr/components/dialog.json index 7e0dda6aa6..4e0fa88431 100644 --- a/web/public/locales/fr/components/dialog.json +++ b/web/public/locales/fr/components/dialog.json @@ -36,6 +36,9 @@ "ask_an": "Cet objet est-il un(e) {{label}} ?", "ask_a": "Cet objet est-il un(e) {{label}} ?", "ask_full": "Cet objet est-il un(e) {{translatedLabel}}  ?" + }, + "toast": { + "error": "Impossible d'envoyer à Frigate+. Merci de vérifier votre réseau puis de réessayer." } } }, @@ -68,7 +71,15 @@ }, "success": "Exportation démarrée avec succès. Consultez le fichier sur la page des exportations.", "view": "Vue", - "queued": "Exportation en attente. Consultez la progression sur la page des exportations." + "queued": "Exportation en attente. Consultez la progression sur la page des exportations.", + "batchSuccess_one": "Un export démarré. Ouverture du dossier en cours.", + "batchSuccess_many": "{{count}} exports démarrés. Ouverture du dossier en cours.", + "batchSuccess_other": "{{count}} exports démarrés. Ouverture du dossier en cours.", + "batchPartial": "{{successful}} exports ont démarré sur un total de : {{total}}. Échecs : {{failedCameras}}", + "batchFailed": "Échec du démarrage de {{total}} exports. Échecs : {{failedCameras}}", + "batchQueuedSuccess_one": "Un export rajouté au traitement. Ouverture du dossier en cours.", + "batchQueuedSuccess_many": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours.", + "batchQueuedSuccess_other": "{{count}} exports rajoutés au traitement. Ouverture du dossier en cours." }, "select": "Sélectionner", "name": { @@ -107,7 +118,14 @@ "queueingButton": "Mise en file d'attente des exports...", "exportButton_one": "Exporter {{count}} caméra", "exportButton_many": "Exporter {{count}} caméras", - "exportButton_other": "Exporter {{count}} caméras" + "exportButton_other": "Exporter {{count}} caméras", + "searchOrSelectGroup": "Chercher ou sélectionner un groupe de caméras...", + "selectAll": "Sélectionner toutes les cameras", + "clearSelection": "Effacer la sélection", + "selectWithActivity": "Caméras avec des objets suivis", + "selectGroup": "Sélectionner un groupe", + "noMatchingCameras": "Aucune caméra ne correspond à votre recherche", + "selectedCount": "{{selected}} / {{total}} sélectionnées" }, "multi": { "title_one": "Export {{count}} revue", diff --git a/web/public/locales/fr/views/motionSearch.json b/web/public/locales/fr/views/motionSearch.json index 5f47e9942a..0c99cad0c6 100644 --- a/web/public/locales/fr/views/motionSearch.json +++ b/web/public/locales/fr/views/motionSearch.json @@ -26,7 +26,9 @@ "points_many": "{{count}} points", "points_other": "{{count}} points", "undo": "Annuler le dernier point", - "reset": "Réinitialiser le polygone" + "reset": "Réinitialiser le polygone", + "drawMode": "Dessiner", + "moveMode": "Déplacer" }, "motionHeatmapLabel": "Carte thermique des mouvements", "dialog": { @@ -36,6 +38,14 @@ }, "timeRange": { "title": "Plage de recherche", - "start": "Plage de recherche" + "start": "Début de la plage de recherche", + "end": "Fin de la plage de recherche" + }, + "settings": { + "title": "Paramètres de recherche", + "parallelMode": "Mode parallèle", + "parallelModeDesc": "Scanner plusieurs plages d'enregistrement simultanément (plus rapide ; nécessite plus de ressources de décodage)", + "threshold": "Seuil de sensitivité", + "thresholdDesc": "Des valeurs basses détectent de plus petits changements (1-255)" } } diff --git a/web/public/locales/fr/views/system.json b/web/public/locales/fr/views/system.json index 74394a324a..3c4577c212 100644 --- a/web/public/locales/fr/views/system.json +++ b/web/public/locales/fr/views/system.json @@ -112,7 +112,7 @@ }, "gpuTemperature": "Température du GPU", "npuTemperature": "Température du NPU", - "gpuCompute": "Calcul / Encodage GPU" + "gpuCompute": "Calcul GPU / Encodage" }, "otherProcesses": { "title": "Autres processus", @@ -122,7 +122,7 @@ "go2rtc": "go2rtc", "recording": "enregistrement", "review_segment": "Segment d'activité", - "embeddings": "embeddings", + "embeddings": "intégrations", "audio_detector": "détecteur audio" } } @@ -173,7 +173,22 @@ "error": "Erreur : {{error}}", "codec": "Codec :", "video": "Vidéo :", - "aspectRatio": "rapport d'aspect" + "aspectRatio": "rapport d'aspect", + "keyframes": { + "title": "Analyse par image clef", + "analyzing": "Analyse des images clefs... {{seconds}} secondes restantes", + "stillAnalyzing": "Analyse des images clefs toujours en cours ...", + "recordStream": "Flux d'enregistrement :", + "keyframeCount": "Images clefs observées :", + "observedDuration": "Durée observée :", + "gap": "Écart en images clefs (min / moy / max) :", + "segmentLength": "Durée du segment d'enregistrement :", + "ok": "Images clefs chaque ~{{seconds}}s, convient à l'enregistrement et au visionnement.", + "warning": "Images clefs fragmentées ou variables (plus grand écart d'environ {{seconds}}s), vraisemblablement un codec intelligent (H.264+/H.265+), non-recommendé.", + "error": "L'écart en images clefs (~{{seconds}}) dépasse la longueur du segment d'enregistrement ({{segmentTime}}). Certains segments n'ont peut-être pas de clef, ce qui corrompt la lecture. Désactiver le codec intelligent/+ sur la caméra ou raccourcir son intervalle en images clefs.", + "unknown": "Impossible de déterminer l'espacement en images clefs.", + "recordDisabled": "L'enregistrement est désactivé pour cette caméra." + } }, "framesAndDetections": "Images / Détections", "label": { @@ -196,7 +211,7 @@ "overview": "Vue d'ensemble", "toast": { "success": { - "copyToClipboard": "Données de la sonde copiées dans le presse-papiers" + "copyToClipboard": "Données de la sonde copiées dans le presse-papiers." }, "error": { "unableToProbeCamera": "Impossible d'interroger la caméra : {{errorMessage}}" @@ -212,6 +227,9 @@ "expectedFps": "IPS attendues", "reconnectsLastHour": "Reconnexions (dernière heure)", "stallsLastHour": "Baisses de qualité (dernière heure)" + }, + "noCameras": { + "title": "Aucune caméra trouvée" } }, "lastRefreshed": "Dernier rafraichissement : ", diff --git a/web/public/locales/he/common.json b/web/public/locales/he/common.json index 1059ae300b..d20ee8b530 100644 --- a/web/public/locales/he/common.json +++ b/web/public/locales/he/common.json @@ -81,7 +81,8 @@ "thisMonth": "החודש", "inProgress": "בתהליך", "invalidStartTime": "זמן התחלה לא תקין", - "invalidEndTime": "זמן סיום לא תקין" + "invalidEndTime": "זמן סיום לא תקין", + "never": "אף פעם" }, "unit": { "speed": { @@ -146,7 +147,8 @@ "download": "הורדה", "info": "מידע", "next": "הבא", - "continue": "המשך" + "continue": "המשך", + "add": "להוסיף" }, "menu": { "system": "מערכת", diff --git a/web/public/locales/hu/audio.json b/web/public/locales/hu/audio.json index 7f31e04592..c16ebfdfc9 100644 --- a/web/public/locales/hu/audio.json +++ b/web/public/locales/hu/audio.json @@ -426,5 +426,12 @@ "chink": "Csörömpölés", "shatter": "Összetörés", "field_recording": "Helyszíni felvétel", - "noise": "Zaj" + "noise": "Zaj", + "liquid": "Folyadék", + "splash": "Csobbanás", + "slosh": "Löttyenés", + "trickle": "Csörgedezés", + "gush": "Folyás", + "stir": "Kavarás", + "thump": "Puffanás" } diff --git a/web/public/locales/hu/views/chat.json b/web/public/locales/hu/views/chat.json index cd90ba0738..0130564979 100644 --- a/web/public/locales/hu/views/chat.json +++ b/web/public/locales/hu/views/chat.json @@ -14,5 +14,6 @@ "attachment_chip_remove": "Melléklet eltávolítása", "open_in_explore": "Megnyitás Böngészőben", "attachment_picker_paste_label": "Vagy illeszd be az esemény ID-t", - "attachment_picker_attach": "Melléklet" + "attachment_picker_attach": "Melléklet", + "arguments": "Argumentumok:" } diff --git a/web/public/locales/hu/views/system.json b/web/public/locales/hu/views/system.json index 73580e256a..f922f61b99 100644 --- a/web/public/locales/hu/views/system.json +++ b/web/public/locales/hu/views/system.json @@ -189,7 +189,11 @@ "face_recognition": "Arcfelismerés", "plate_recognition": "Rendszám Felismerés", "yolov9_plate_detection_speed": "YOLOv9 Rendszám Felismerés Sebesség", - "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés" + "yolov9_plate_detection": "YOLOv9 Rendszám Észlelés", + "classification_events_per_second": "{{name}} Osztályozási események másodpercenként", + "classification_speed": "{{name}} Osztályozási sebesség", + "classification": "{{name}} Osztályozás", + "object_description_events_per_second": "Objektum leírás" }, "infPerSecond": "Inferencia Per Másodperc", "title": "Kiegészítések" diff --git a/web/public/locales/id/common.json b/web/public/locales/id/common.json index 455b3f4bf4..cdeb4ab24b 100644 --- a/web/public/locales/id/common.json +++ b/web/public/locales/id/common.json @@ -126,7 +126,8 @@ "ur": "اردو (Urdu)", "withSystem": { "label": "Gunakan pengaturan sistem untuk bahasa" - } + }, + "zhHant": "繁體中文 (Tionghoa Tradisional)" }, "appearance": "Tampilan", "darkMode": { diff --git a/web/public/locales/id/components/camera.json b/web/public/locales/id/components/camera.json index de7759f34d..61ca36bf21 100644 --- a/web/public/locales/id/components/camera.json +++ b/web/public/locales/id/components/camera.json @@ -42,12 +42,49 @@ "stream": "Siaran", "placeholder": "Pilih stream", "streamMethod": { - "label": "Metode Streaming" + "label": "Metode Streaming", + "placeholder": "Pilih satu metode streaming", + "method": { + "noStreaming": { + "label": "Tidak Streaming", + "desc": "Gambar kamera hanya diperbarui sekali per menit dan tidak akan ada live streaming." + }, + "smartStreaming": { + "label": "Streaming cerdas (disarankan)", + "desc": "Streaming cerdas akan memperbarui gambar kamera sekali per menit saat tidak ada aktivitas yang terdeteksi untuk menghemat bandwidth dan sumber daya. Ketika aktivitas terdeteksi, gambar akan otomatis beralih ke live stream." + }, + "continuousStreaming": { + "desc": { + "title": "Kamera akan selalu live stream selama muncul di dashboard, meskipun tidak ada aktivitas yang terdeteksi.", + "warning": "Streaming Terus Menerus dapat menyebabkan penggunaan bandwidth yang tinggi dan masalah performa. Gunakan dengan hati-hati." + }, + "label": "Streaming Terus Menerus" + } + } + }, + "compatibilityMode": { + "label": "Mode Kompatibilitas", + "desc": "Aktifkan opsi ini hanya jika live stream kamera Anda menampilkan artefak warna dan memiliki garis diagonal di sisi kanan gambar." } } - } + }, + "showAll": "Tampilkan semua grup kamera", + "showLess": "Lebih sedikit", + "editGroups": "Sunting Grup Kamera" }, "debug": { - "boundingBox": "Batas Kotak" + "boundingBox": "Batas Kotak", + "options": { + "label": "Pengaturan", + "title": "Opsi", + "showOptions": "Lihat Opsi", + "hideOptions": "Sembunyikan Opsi" + }, + "zones": "Zona", + "motion": "Gerakan", + "regions": "Wilayah", + "timestamp": "Timestamp", + "mask": "Masker", + "paths": "Path" } } diff --git a/web/public/locales/id/components/dialog.json b/web/public/locales/id/components/dialog.json index 35d87b07ca..6ec42c5759 100644 --- a/web/public/locales/id/components/dialog.json +++ b/web/public/locales/id/components/dialog.json @@ -52,12 +52,73 @@ "export": "Ekspor", "selectOrExport": "Pilih atau Ekspor", "toast": { - "success": "Berhasil memulai ekspor. Lihat file pada halaman ekspor." - } + "success": "Berhasil memulai ekspor. Lihat file pada halaman ekspor.", + "queued": "Ekspor diantrekan. Lihat progres di halaman ekspor.", + "view": "Melihat", + "batchSuccess_other": "{{count}} Ekspor dimulai. Membuka kasusnya sekarang.", + "batchPartial": "Ekspor berhasil dimulai sebanyak {{successful}} dari total {{total}} ekspor. Kamera yang gagal: {{failedCameras}}", + "batchFailed": "Gagal memulai ekspor sebanyak {{total}}. Kamera yang gagal: {{failedCameras}}" + }, + "case": { + "newCaseOption": "Membuat Kasus Baru", + "newCaseNamePlaceholder": "Nama Kasus Baru", + "newCaseDescriptionPlaceholder": "Deskripsi Kasus", + "label": "Kasus", + "nonAdminHelp": "Kasus Baru akan Dibuat untuk Ekspor ini.", + "placeholder": "Pilih Kasus" + }, + "tabs": { + "export": "Kamera Tunggal", + "multiCamera": "Multi Kamera" + }, + "multiCamera": { + "timeRange": "Rentang Waktu", + "selectFromTimeline": "Pilih dari Timeline", + "cameraSelection": "Kamera", + "checkingActivity": "Memeriksa Aktivitas Kamera...", + "noCameras": "Tidak ada Kamera Tersedia", + "detectionCount_other": "{{count}} Objek terlacak", + "nameLabel": "Nama Ekspor", + "exportButton_other": "Ekspor {{count}} kamera", + "cameraSelectionHelp": "Kamera dengan objek yang dilacak dalam rentang waktu ini telah dipilih sebelumnya", + "queueingButton": "Mengantrekan ekspor...", + "clearSelection": "Hapus pilihan", + "selectWithActivity": "Kamera dengan objek yang dilacak", + "selectGroup": "Pilih grup", + "noMatchingCameras": "Tidak ada kamera yang sesuai dengan pencarian Anda", + "selectedCount": "{{terpilih}} / {{total}} terpilih", + "namePlaceholder": "Nama dasar opsional untuk ekspor ini" + }, + "multi": { + "title_other": "Ekspor {{count}} Ulasan", + "toast": { + "startedNoCase_other": "{{count}} Ekspor Dimulai.", + "started_other": "{{count}} ekspor dimulai. Membuka kasusnya sekarang.", + "partial": "Memulai {{successful}} dari {{total}} ekspor. Gagal: {{failedItems}}", + "failed": "Gagal memulai {{total}} ekspor. Gagal: {{failedItems}}" + }, + "description": "Ekspor setiap ulasan yang dipilih. Semua Ekspor akan dikelompokkan dalam satu kasus.", + "descriptionNoCase": "Ekspor setiap ulasan yang dipilih.", + "exportButton_other": "Ekspor {{count}} ulasan", + "exportingButton": "Mengekspor...", + "caseNamePlaceholder": "Ekspor tinjauan - {{tanggal}}" + }, + "queueing": "Mengantrekan Ekspor..." }, "search": { "saveSearch": { "overwrite": "{{searchName}} sudah ada. Menyimpan akan menimpa file yang sudah ada." } + }, + "streaming": { + "restreaming": { + "disabled": "Fitur restreaming tidak diaktifkan untuk kamera ini.", + "desc": { + "title": "Instal go2rtc untuk opsi tampilan langsung dan audio tambahan untuk kamera ini." + } + }, + "showStats": { + "label": "Tampilkan statistik streaming" + } } } diff --git a/web/public/locales/it/config/cameras.json b/web/public/locales/it/config/cameras.json index e359290754..98149073c7 100644 --- a/web/public/locales/it/config/cameras.json +++ b/web/public/locales/it/config/cameras.json @@ -132,53 +132,111 @@ } }, "mqtt": { - "label": "MQTT" + "label": "MQTT", + "description": "Impostazioni di pubblicazione delle immagini via MQTT.", + "enabled": { + "label": "Invia immagine", + "description": "Abilita la pubblicazione delle istantanee dell'immagine per gli oggetti sui topic MQTT per questa telecamera." + }, + "timestamp": { + "label": "Aggiungi timestamp", + "description": "Sovraimprime un timestamp sulle immagini pubblicate su MQTT." + }, + "bounding_box": { + "label": "Aggiungi riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione sulle immagini pubblicate tramite MQTT." + }, + "crop": { + "label": "Ritaglia immagine", + "description": "Ritaglia le immagini pubblicate su MQTT in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "height": { + "label": "Altezza immagine", + "description": "Altezza (in pixel) a cui ridimensionare le immagini pubblicate tramite MQTT." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga pubblicata un'immagine MQTT." + }, + "quality": { + "label": "Qualità JPEG", + "description": "Qualità JPEG per le immagini pubblicate su MQTT (0-100)." + } }, "onvif": { "tls_insecure": { - "label": "Disabilita verifica TLS" + "label": "Disabilita verifica TLS", + "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)." }, "profile": { - "label": "Profilo ONVIF" + "label": "Profilo ONVIF", + "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida." }, "autotracking": { "label": "Tracciamento automatico", "enabled": { - "label": "Abilita il tracciamento automatico" + "label": "Abilita il tracciamento automatico", + "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati." }, "calibrate_on_startup": { - "label": "Calibra all'avvio" + "label": "Calibra all'avvio", + "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione." }, "zooming": { - "label": "Modalità ingrandimento" + "label": "Modalità ingrandimento", + "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)." }, "zoom_factor": { - "label": "Fattore di ingrandimento" + "label": "Fattore di ingrandimento", + "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75." }, "track": { "label": "Oggetti tracciati", "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico." }, "required_zones": { - "label": "Zone richieste" + "label": "Zone richieste", + "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico." }, "timeout": { "label": "Scadenza di ritorno", "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata." }, "movement_weights": { - "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente." + "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.", + "label": "Pesi del movimento" }, "enabled_in_config": { - "label": "Stato originale del tracciamento automatico" + "label": "Stato originale del tracciamento automatico", + "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione." + }, + "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.", + "return_preset": { + "label": "Preset di ritorno", + "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento." } }, "ignore_time_mismatch": { - "label": "Ignora la discrepanza oraria" + "label": "Ignora la discrepanza oraria", + "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF." }, "label": "ONVIF", "port": { - "label": "Porta ONVIF" + "label": "Porta ONVIF", + "description": "Numero di porta per il servizio ONVIF." + }, + "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.", + "host": { + "label": "Host ONVIF", + "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera." + }, + "user": { + "label": "Nome utente ONVIF", + "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF." + }, + "password": { + "label": "Password ONVIF", + "description": "Password per l'autenticazione ONVIF." } }, "detect": { @@ -363,20 +421,20 @@ "description": "Numero massimo di processi di esportazione da elaborare contemporaneamente." } }, - "description": "Impostazioni di registrazione e conservazione per questa videocamera.", + "description": "Impostazioni di registrazione e conservazione per questa telecamera.", "enabled": { - "label": "Abilita Registrazione", + "label": "Abilita registrazione", "description": "Attiva o disattiva la registrazione per questa telecamera." }, "expire_interval": { - "label": "Intervallo pulizia registrazioni", + "label": "Intervallo di pulizia registrazioni", "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti." }, "continuous": { "label": "Conservazione continua", "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.", "days": { - "label": "Giorni di Conservazione", + "label": "Giorni di conservazione", "description": "Numero di giorni di conservazione delle registrazioni." } }, @@ -384,7 +442,7 @@ "label": "Conservazione movimento", "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.", "days": { - "label": "Giorni di Conservazione", + "label": "Giorni di conservazione", "description": "Numero di giorni di conservazione delle registrazioni." } }, @@ -392,12 +450,12 @@ "label": "Conservazione rilevamento", "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.", "pre_capture": { - "label": "Secondi di pre-acquisizione", - "description": "Numero di secondi precedenti l'evento di rilevamento da includere nella registrazione." + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." }, "post_capture": { - "label": "Secondi di post-acquisizione", - "description": "Numero di secondi successivi l'evento di rilevamento da includere nella registrazione." + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." }, "retain": { "label": "Conservazione eventi", @@ -414,13 +472,13 @@ }, "alerts": { "label": "Conservazione avvisi", - "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate di pre e post-acquisizione.", + "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.", "pre_capture": { - "label": "Secondi di pre-acquisizione", - "description": "Numero di secondi antecedenti all'evento di rilevamento da includere nella registrazione." + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." }, "post_capture": { - "label": "Secondi di post-acquisizione", + "label": "Secondi dopo la cattura", "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." }, "retain": { @@ -461,7 +519,40 @@ "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API." }, "bounding_box": { - "label": "Sovrimpressione riquadro di delimitazione" + "label": "Sovrimpressione riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API." + }, + "crop": { + "label": "Ritaglia istantanea", + "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea." + }, + "height": { + "label": "Altezza istantanea", + "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale." + }, + "retain": { + "label": "Conservazione istantanee", + "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.", + "default": { + "label": "Conservazione predefinita", + "description": "Numero predefinito di giorni per cui conservare le istantanee." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + }, + "objects": { + "label": "Conservazione oggetti", + "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee." + } + }, + "quality": { + "label": "Qualità istantanee", + "description": "Qualità di codifica per le istantanee salvate (0-100)." } }, "motion": { @@ -492,10 +583,12 @@ "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione." }, "delta_alpha": { - "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento." + "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.", + "label": "Delta alfa" }, "frame_alpha": { - "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento." + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.", + "label": "Frame alfa" }, "frame_height": { "label": "Altezza del frame", @@ -510,7 +603,8 @@ "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT." }, "enabled_in_config": { - "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale." + "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.", + "label": "Stato movimento originale" }, "raw_mask": { "label": "Maschera grezza" @@ -548,11 +642,11 @@ }, "min_score": { "label": "Confidenza minima", - "description": "Livello minimo di confidenza del rilevamento per singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." }, "mask": { "label": "Maschera filtro", - "description": "Coordinate del poligono che definisce l'area all'interno dell'inquadratura in cui si applica questo filtro." + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." }, "raw_mask": { "label": "Maschera grezza" @@ -560,40 +654,42 @@ }, "mask": { "label": "Maschera oggetto", - "description": "Poligono di maschera utilizzato per impedire il rilevamento di oggetti in aree specificate." + "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche." }, "genai": { "label": "Configurazione degli oggetti GenAI", "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.", "enabled": { "label": "Abilita GenAI", - "description": "Abilita di default la generazione tramite GenAI delle descrizioni degli oggetti monitorati." + "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati." }, "use_snapshot": { - "label": "Usa istantanee", + "label": "Utilizza istantanee", "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." }, "prompt": { - "description": "Modello di prompt predefinito utilizzato per la generazione di descrizioni con GenAI." + "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.", + "label": "Richiesta di didascalia" }, "object_prompts": { - "description": "Prompt specifici per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche." + "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.", + "label": "Richiesta di oggetto" }, "objects": { "label": "Oggetti GenAI", - "description": "Elenco delle etichette degli oggetti da inviare a GenAI per impostazione predefinita." + "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita." }, "required_zones": { - "label": "Zone obbligatorie", - "description": "Zone che devono essere inserite affinché gli oggetti possano essere utilizzati per la generazione di descrizioni tramite GenAI." + "label": "Zone richieste", + "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI." }, "debug_save_thumbnails": { "label": "Salva miniature", - "description": "Salva le miniature inviate a GenAI per il debug e la revisione." + "description": "Salva le miniature inviate a GenAI per la correzione e la revisione." }, "send_triggers": { - "label": "Attivatori GenAI", - "description": "Definisce quando i frame devono essere inviati a GenAI (al termine, dopo gli aggiornamenti, ecc.).", + "label": "Attivazioni GenAI", + "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).", "tracked_object_end": { "label": "Invia alla fine", "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina." @@ -629,25 +725,60 @@ } }, "timestamp_style": { - "label": "Stile orario" + "label": "Stile orario", + "description": "Opzioni di stile per i timestamp applicati alle istantanee e alla vista di Debug.", + "position": { + "label": "Posizione timestamp", + "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)." + }, + "format": { + "label": "Formato timestamp", + "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)." + }, + "color": { + "label": "Colore timestamp", + "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).", + "red": { + "label": "Rosso", + "description": "Componente rosso (0-255) per il colore del timestamp." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) per il colore del timestamp." + }, + "blue": { + "label": "Blu", + "description": "Componente blu (0-255) per il colore del timestamp." + } + }, + "thickness": { + "label": "Spessore timestamp", + "description": "Spessore della linea del testo del timestamp." + }, + "effect": { + "label": "Effetto timestamp", + "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)." + } }, "notifications": { "label": "Notifiche", "enabled": { - "label": "Abilita le notifiche" + "label": "Abilita le notifiche", + "description": "Abilita o disabilita le notifiche per questa telecamera." }, "email": { "label": "Email di notifica", "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica." }, "cooldown": { - "label": "Periodo di raffreddamento", + "label": "Tempo di attesa tra notifiche", "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari." }, "enabled_in_config": { "label": "Stato delle notifiche originali", "description": "Indica se le notifiche erano abilitate nella configurazione statica originale." - } + }, + "description": "Impostazioni per abilitare e controllare le notifiche per questa telecamera." }, "birdseye": { "label": "Birdseye", @@ -671,7 +802,7 @@ "label": "Attivatori", "friendly_name": { "label": "Nome descrittivo", - "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo innesco." + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore." }, "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.", "enabled": { @@ -720,17 +851,23 @@ "ui": { "description": "Visualizza l'ordine e la visibilità di questa telecamera nell'interfaccia utente. L'ordine influisce sul cruscotto predefinito. Per un controllo più granulare, utilizza i gruppi di telecamere.", "order": { - "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente." + "description": "L'ordine numerico viene utilizzato per ordinare le telecamere nell'interfaccia utente (cruscotto ed elenchi predefiniti); i numeri più grandi compaiono successivamente.", + "label": "Ordine interfaccia utente" }, "dashboard": { "label": "Mostra nell'interfaccia utente", "description": "Abilita o disabilita la visualizzazione di questa telecamera in ogni punto dell'interfaccia utente di Frigate. Disabilitando questa opzione, sarà necessario modificare manualmente la configurazione per visualizzare nuovamente la telecamera nell'interfaccia utente." }, - "label": "Interfaccia utente telecamera" + "label": "Interfaccia utente telecamera", + "review": { + "label": "Mostra in revisione", + "description": "Abilita o disabilita la visibilità di questa telecamera nella sezione di revisione (la pagina di revisione e il relativo filtro telecamera, la revisione del movimento e la vista della cronologia)." + } }, "zones": { "enabled": { - "label": "Abilitata" + "label": "Abilitata", + "description": "Abilita o disabilita questa zona. Le zone disabilitate vengono ignorate durante l'esecuzione." }, "label": "Zone", "friendly_name": { @@ -740,11 +877,81 @@ "filters": { "raw_mask": { "label": "Maschera grezza" + }, + "label": "Filtri Zona", + "description": "Filtri da applicare agli oggetti all'interno di questa zona. Utilizzati per ridurre i falsi positivi o limitare quali oggetti sono considerati presenti nella zona.", + "min_area": { + "label": "Area minima oggetto", + "description": "Area minima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere in pixel (int) o in percentuale (float compreso tra 0.000001 e 0.99)." + }, + "max_area": { + "label": "Area massima oggetto", + "description": "Area massima del riquadro di delimitazione (in pixel o percentuale) richiesta per questo tipo di oggetto. Può essere espressa in pixel (numero intero) o in percentuale (valore decimale compreso tra 0.000001 e 0.99)." + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza", + "description": "Soglia di confidenza media di rilevamento necessaria affinché l'oggetto sia considerato un vero positivo." + }, + "min_score": { + "label": "Confidenza minima", + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." } + }, + "description": "Le zone consentono di definire un'area specifica dell'inquadratura per determinare se un oggetto si trova o meno all'interno di una particolare area.", + "enabled_in_config": { + "label": "Mantieni traccia dello stato originale della zona." + }, + "coordinates": { + "label": "Coordinate", + "description": "Coordinate del poligono che definiscono l'area della zona. Può essere una stringa separata da virgole o un elenco di stringhe di coordinate. Le coordinate devono essere relative (0-1) o assolute (legacy)." + }, + "distances": { + "label": "Distanze reali", + "description": "Distanze reali opzionali per ciascun lato del quadrilatero della zona, utilizzate per i calcoli di velocità o distanza. Deve avere esattamente 4 valori se impostato." + }, + "inertia": { + "label": "Fotogrammi di inerzia", + "description": "Numero di fotogrammi consecutivi in cui un oggetto deve essere rilevato nella zona prima di essere considerato presente. Aiuta a filtrare i rilevamenti transitori." + }, + "loitering_time": { + "label": "Secondi di stazionamento", + "description": "Numero di secondi per cui un oggetto deve rimanere nella zona per essere considerato in stazionamento nella stessa. Imposta a 0 per disabilitare il rilevamento dello stazionamento." + }, + "speed_threshold": { + "label": "Velocità minima", + "description": "Velocità minima (in unità reali se le distanze sono impostate) richiesta affinché un oggetto sia considerato presente nella zona. Utilizzata per gli attivatori di zona basati sulla velocità." + }, + "objects": { + "label": "Oggetti attivatori", + "description": "Elenco dei tipi di oggetto (dalla labelmap) che possono attivare questa zona. Può essere una stringa o un elenco di stringhe. Se vuoto, vengono considerati tutti gli oggetti." } }, "type": { "description": "Tipo di telecamera", "label": "Tipo di telecamera" + }, + "best_image_timeout": { + "label": "Timeout immagine migliore", + "description": "Quanto tempo attendere per l'immagine con il punteggio di accuratezza più alto." + }, + "webui_url": { + "label": "URL telecamera", + "description": "URL per visitare direttamente la telecamera dalla pagina di sistema" + }, + "enabled_in_config": { + "label": "Stato telecamera originale", + "description": "Mantiene traccia dello stato originale della telecamera." } } diff --git a/web/public/locales/it/config/global.json b/web/public/locales/it/config/global.json index e3045ddc56..b8c557ce3c 100644 --- a/web/public/locales/it/config/global.json +++ b/web/public/locales/it/config/global.json @@ -99,6 +99,10 @@ "hash_iterations": { "description": "Numero di iterazioni PBKDF2-SHA256 da utilizzare per criptare le password utente.", "label": "Iterazioni di crittografia" + }, + "admin_first_time_login": { + "label": "Flag primo accesso amministratore", + "description": "Quando impostato su true, l'interfaccia utente può mostrare un link di aiuto nella pagina di login per informare gli utenti su come accedere dopo un ripristino della password di amministratore. " } }, "ffmpeg": { @@ -175,11 +179,17 @@ "label": "Numero di processi di rilevamento", "description": "Il numero di processi utilizzati per l'inferenza basata sulla CPU." }, - "description": "Rilevatore CPU TFLite che esegue modelli TensorFlow Lite sulla CPU di sistema senza accelerazione hardware. Sconsigliato." + "description": "Rilevatore CPU TFLite che esegue modelli TensorFlow Lite sulla CPU di sistema senza accelerazione hardware. Sconsigliato.", + "label": "CPU" }, "label": "Dispositivo di rilevamento", "hailo8l": { - "description": "Rilevatore Hailo-8/Hailo-8L che utilizza modelli HEF e l'SDK HailoRT per l'inferenza sul dispositivo Hailo." + "description": "Rilevatore Hailo-8/Hailo-8L che utilizza modelli HEF e l'SDK HailoRT per l'inferenza sul dispositivo Hailo.", + "label": "Hailo-8/Hailo-8L", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza Hailo (ad es. 'PCIe', 'M.2')." + } }, "openvino": { "description": "Rilevatore OpenVINO per CPU AMD e Intel, GPU Intel e dispositivo Intel VPU." @@ -192,6 +202,111 @@ }, "type": { "label": "Tipo" + }, + "description": "Configurazione per i rilevatori di oggetti (backend CPU, GPU, ONNX) e impostazioni del modello specifiche per il rilevatore.", + "model": { + "label": "Configurazione del modello specifica del rilevatore", + "description": "Opzioni di configurazione del modello specifiche del rilevatore (percorso, dimensioni di input, ecc.).", + "path": { + "label": "Percorso del modello di rilevamento oggetti personalizzato", + "description": "Percorso di un file del modello di rilevamento personalizzato (o plus:// per i modelli Frigate+)." + }, + "labelmap_path": { + "label": "Mappa delle etichette (label map) per il rilevatore di oggetti personalizzato", + "description": "Percorso di un file labelmap che mappa le classi numeriche in etichette testuali per il rilevatore." + }, + "width": { + "label": "Larghezza di input del modello di rilevamento oggetti", + "description": "Larghezza del tensore di input del modello in pixel." + }, + "height": { + "label": "Altezza di input del modello di rilevamento oggetti", + "description": "Altezza del tensore di input del modello in pixel." + }, + "labelmap": { + "label": "Personalizzazione labelmap", + "description": "Sostituzioni o voci di rimappatura da unire alla labelmap standard." + }, + "attributes_map": { + "label": "Mappa delle etichette oggetto con le relative etichette di attributo", + "description": "Mappatura dalle etichette oggetto alle etichette di attributo utilizzata per allegare metadati (ad esempio 'car' -> ['license_plate'])." + }, + "input_tensor": { + "label": "Forma del tensore di input del modello", + "description": "Formato del tensore richiesto dal modello: 'nhwc' o 'nchw'." + }, + "input_pixel_format": { + "label": "Formato colore dei pixel di input del modello", + "description": "Spazio colore dei pixel richiesto dal modello: 'rgb', 'bgr' o 'yuv'." + }, + "input_dtype": { + "label": "Tipo di dati di input del modello (Dtype)", + "description": "Tipo di dati del tensore di input del modello (ad esempio 'float32')." + }, + "model_type": { + "label": "Tipo di modello di rilevamento oggetti", + "description": "Tipo di architettura del modello del rilevatore (ssd, yolox, yolonas) utilizzato da alcuni rilevatori per l'ottimizzazione." + } + }, + "model_path": { + "label": "Percorso del modello specifico del rilevatore", + "description": "Percorso del file binario del modello del rilevatore, se richiesto dal rilevatore scelto." + }, + "axengine": { + "label": "NPU AXEngine", + "description": "Rilevatore NPU AXERA AX650N/AX8850N che esegue file .axmodel compilati tramite il runtime AXEngine." + }, + "deepstack": { + "label": "DeepStack", + "description": "Rilevatore DeepStack/CodeProject.AI che invia immagini a una API HTTP DeepStack remota per l'inferenza. Non consigliato.", + "api_url": { + "label": "URL API DeepStack", + "description": "L'URL delle API DeepStack." + }, + "api_timeout": { + "label": "Timeout API DeepStack (in secondi)", + "description": "Tempo massimo consentito per una richiesta alle API DeepStack." + }, + "api_key": { + "label": "Chiave API DeepStack (se richiesta)", + "description": "Chiave API opzionale per i servizi DeepStack autenticati." + } + }, + "degirum": { + "label": "DeGirum", + "description": "Rilevatore DeGirum per l'esecuzione di modelli tramite il cloud DeGirum o servizi di inferenza locali.", + "zoo": { + "label": "Model Zoo", + "description": "Percorso o URL del model zoo di DeGirum." + }, + "token": { + "label": "Token Cloud DeGirum", + "description": "Token per l'accesso al Cloud DeGirum." + } + }, + "edgetpu": { + "label": "EdgeTPU", + "description": "Rilevatore EdgeTPU che esegue modelli TensorFlow Lite compilati per Coral EdgeTPU utilizzando il delegate EdgeTPU.", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza EdgeTPU (ad es. 'usb', 'pci')." + } + }, + "memryx": { + "label": "MemryX", + "description": "Rilevatore MemryX MX3 che esegue modelli DFP compilati su acceleratori MemryX.", + "device": { + "label": "Percorso del dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza MemryX (ad es. 'PCIe')." + } + }, + "onnx": { + "label": "ONNX", + "description": "Rilevatore ONNX per l'esecuzione di modelli ONNX; utilizzerà i backend di accelerazione disponibili (CUDA/ROCm/OpenVINO) quando presenti.", + "device": { + "label": "Tipo di dispositivo", + "description": "Il dispositivo da utilizzare per l'inferenza ONNX (ad es. 'AUTO', 'CPU', 'GPU')." + } } }, "audio_transcription": { @@ -266,49 +381,78 @@ }, "onvif": { "tls_insecure": { - "label": "Disabilita verifica TLS" + "label": "Disabilita verifica TLS", + "description": "Salta la verifica TLS e disabilita l'autenticazione digest per ONVIF (non sicuro; utilizzare solo in reti sicure)." }, "profile": { - "label": "Profilo ONVIF" + "label": "Profilo ONVIF", + "description": "Profilo multimediale ONVIF specifico da utilizzare per il controllo PTZ, identificato tramite token o nome. Se non impostato, viene selezionato automaticamente il primo profilo con una configurazione PTZ valida." }, "autotracking": { "label": "Tracciamento automatico", "enabled": { - "label": "Abilita il tracciamento automatico" + "label": "Abilita il tracciamento automatico", + "description": "Abilita o disabilita il tracciamento automatico della telecamera PTZ per gli oggetti rilevati." }, "calibrate_on_startup": { - "label": "Calibra all'avvio" + "label": "Calibra all'avvio", + "description": "Misura la velocità dei motori PTZ all'avvio per migliorare la precisione del tracciamento. Frigate aggiornerà la configurazione con i valori di movement_weights dopo la calibrazione." }, "zooming": { - "label": "Modalità ingrandimento" + "label": "Modalità ingrandimento", + "description": "Controlla il comportamento dello zoom: disabled (solo pan/tilt), absolute (più compatibile) o relative (pan/tilt/zoom simultanei)." }, "zoom_factor": { - "label": "Fattore di ingrandimento" + "label": "Fattore di ingrandimento", + "description": "Controlla il livello di zoom sugli oggetti tracciati. Valori più bassi mantengono una porzione maggiore della scena visibile; valori più alti aumentano lo zoom ma potrebbero far perdere il tracciamento. Valori compresi tra 0.1 e 0.75." }, "track": { "label": "Oggetti tracciati", "description": "Elenco dei tipi di oggetto che dovrebbero attivare il tracciamento automatico." }, "required_zones": { - "label": "Zone richieste" + "label": "Zone richieste", + "description": "Gli oggetti devono entrare in una di queste zone prima che inizi il tracciamento automatico." }, "timeout": { "label": "Scadenza di ritorno", "description": "Attendi questo numero di secondi dopo aver perso il tracciamento prima di riportare la telecamera nella posizione preimpostata." }, "movement_weights": { - "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente." + "description": "Valori di calibrazione generati automaticamente dalla calibrazione della telecamera. Non modificare manualmente.", + "label": "Pesi del movimento" }, "enabled_in_config": { - "label": "Stato originale del tracciamento automatico" + "label": "Stato originale del tracciamento automatico", + "description": "Campo interno per tracciare se il tracciamento automatico era abilitato nella configurazione." + }, + "description": "Traccia automaticamente gli oggetti in movimento e li mantiene centrati nell'inquadratura utilizzando i movimenti della telecamera PTZ.", + "return_preset": { + "label": "Preset di ritorno", + "description": "Nome del preset ONVIF configurato nel firmware della telecamera a cui ritornare al termine del tracciamento." } }, "ignore_time_mismatch": { - "label": "Ignora la discrepanza oraria" + "label": "Ignora la discrepanza oraria", + "description": "Ignora le differenze di sincronizzazione oraria tra la telecamera e il server Frigate per la comunicazione ONVIF." }, "label": "ONVIF", "port": { - "label": "Porta ONVIF" + "label": "Porta ONVIF", + "description": "Numero di porta per il servizio ONVIF." + }, + "description": "Impostazioni della connessione ONVIF e del tracciamento automatico PTZ per questa telecamera.", + "host": { + "label": "Host ONVIF", + "description": "Host (e schema opzionale) per il servizio ONVIF di questa telecamera." + }, + "user": { + "label": "Nome utente ONVIF", + "description": "Nome utente per l'autenticazione ONVIF; alcuni dispositivi richiedono un utente amministratore per l'ONVIF." + }, + "password": { + "label": "Password ONVIF", + "description": "Password per l'autenticazione ONVIF." } }, "detect": { @@ -413,7 +557,11 @@ }, "default_role": { "label": "Ruolo predefinito", - "description": "Ruolo predefinito assegnato agli utenti autenticati tramite proxy quando non si applica alcuna mappatura dei ruoli (amministratore o visualizzatore)." + "description": "Ruolo predefinito assegnato agli utenti autenticati tramite proxy quando non si applica nessuna mappatura dei ruoli." + }, + "separator": { + "label": "Carattere separatore", + "description": "Carattere utilizzato per separare valori multipli forniti negli header del proxy." } }, "review": { @@ -507,7 +655,19 @@ }, "ui": { "label": "Interfaccia utente", - "description": "Preferenze dell'interfaccia utente come fuso orario, formato di data/ora e unità di misura." + "description": "Preferenze dell'interfaccia utente come fuso orario, formato di data/ora e unità di misura.", + "timezone": { + "label": "Fuso orario", + "description": "Fuso orario opzionale da visualizzare nell'interfaccia utente (se non impostato, viene usata l'ora locale del browser)." + }, + "time_format": { + "label": "Formato Ora", + "description": "Formato dell'ora da utilizzare nell'interfaccia utente (browser, 12 ore o 24 ore)." + }, + "unit_system": { + "label": "Sistema di unità di misura", + "description": "Sistema di unità di misura per la visualizzazione (metrico o imperiale) utilizzato nell'interfaccia utente e nei messaggi MQTT." + } }, "profiles": { "label": "Profili", @@ -532,17 +692,18 @@ } }, "enabled": { - "label": "Abilita Registrazione" + "label": "Abilita registrazione", + "description": "Attiva o disattiva la registrazione per tutte le telecamere; l'impostazione può essere modificata per ogni singola telecamera." }, "expire_interval": { - "label": "Intervallo pulizia registrazioni", + "label": "Intervallo di pulizia registrazioni", "description": "Minuti tra i cicli di pulizia che rimuovono i segmenti di registrazione scaduti." }, "continuous": { "label": "Conservazione continua", "description": "Numero di giorni per cui conservare le registrazioni, indipendentemente dagli oggetti tracciati o dal movimento. Impostare su 0 se si desidera conservare solo le registrazioni relative agli avvisi e ai rilevamenti.", "days": { - "label": "Giorni di Conservazione", + "label": "Giorni di conservazione", "description": "Numero di giorni di conservazione delle registrazioni." } }, @@ -550,7 +711,7 @@ "label": "Conservazione movimento", "description": "Numero di giorni per cui conservare le registrazioni attivate dal movimento, indipendentemente dagli oggetti tracciati. Impostare su 0 se si desidera conservare solo le registrazioni relative ad allarmi e rilevamenti.", "days": { - "label": "Giorni di Conservazione", + "label": "Giorni di conservazione", "description": "Numero di giorni di conservazione delle registrazioni." } }, @@ -558,12 +719,12 @@ "label": "Conservazione rilevamento", "description": "Impostazioni relative alla conservazione delle registrazioni per gli eventi di rilevamento, comprese le durate prima e dopo l'acquisizione.", "pre_capture": { - "label": "Secondi di pre-acquisizione", - "description": "Numero di secondi precedenti l'evento di rilevamento da includere nella registrazione." + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." }, "post_capture": { - "label": "Secondi di post-acquisizione", - "description": "Numero di secondi successivi l'evento di rilevamento da includere nella registrazione." + "label": "Secondi dopo la cattura", + "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." }, "retain": { "label": "Conservazione eventi", @@ -580,13 +741,13 @@ }, "alerts": { "label": "Conservazione avvisi", - "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate di pre e post-acquisizione.", + "description": "Impostazioni di conservazione delle registrazioni per gli eventi di avviso, incluse le durate prima e dopo la cattura.", "pre_capture": { - "label": "Secondi di pre-acquisizione", - "description": "Numero di secondi antecedenti all'evento di rilevamento da includere nella registrazione." + "label": "Secondi prima della cattura", + "description": "Numero di secondi precedenti all'evento di rilevamento da includere nella registrazione." }, "post_capture": { - "label": "Secondi di post-acquisizione", + "label": "Secondi dopo la cattura", "description": "Numero di secondi successivi all'evento di rilevamento da includere nella registrazione." }, "retain": { @@ -613,7 +774,8 @@ "enabled_in_config": { "label": "Stato registrazione originale", "description": "Indica se la registrazione era abilitata nella configurazione statica originale." - } + }, + "description": "Impostazioni di registrazione e conservazione vengono applicate alle telecamere a meno che non vengano modificate singolarmente per ciascuna telecamera." }, "snapshots": { "label": "Istantanee", @@ -625,7 +787,40 @@ "description": "Sovraimprime un timestamp sulle istantanee provenienti dalle API." }, "bounding_box": { - "label": "Sovrimpressione riquadro di delimitazione" + "label": "Sovrimpressione riquadro di delimitazione", + "description": "Disegna i riquadri di delimitazione per gli oggetti tracciati sulle istantanee provenienti dalle API." + }, + "crop": { + "label": "Ritaglia istantanea", + "description": "Ritaglia le istantanee provenienti dalle API in base al riquadro di delimitazione dell'oggetto rilevato." + }, + "required_zones": { + "label": "Zone richieste", + "description": "Zone in cui un oggetto deve entrare affinché venga salvata un'istantanea." + }, + "height": { + "label": "Altezza istantanea", + "description": "Altezza (in pixel) a cui ridimensionare le istantanee provenienti dalle API; lascia vuoto per mantenere la dimensione originale." + }, + "retain": { + "label": "Conservazione istantanee", + "description": "Impostazioni di conservazione per le istantanee, inclusi i giorni predefiniti e le eccezioni per singolo oggetto.", + "default": { + "label": "Conservazione predefinita", + "description": "Numero predefinito di giorni per cui conservare le istantanee." + }, + "mode": { + "label": "Modalità di conservazione", + "description": "Modalità di conservazione: tutti (salva tutti i segmenti), movimento (salva i segmenti con movimento) o oggetti_attivi (salva i segmenti con oggetti attivi)." + }, + "objects": { + "label": "Conservazione oggetti", + "description": "Eccezioni per singolo oggetto relativi ai giorni di conservazione delle istantanee." + } + }, + "quality": { + "label": "Qualità istantanee", + "description": "Qualità di codifica per le istantanee salvate (0-100)." } }, "motion": { @@ -654,10 +849,12 @@ "description": "Se impostato su un valore compreso tra 0.0 e 110, e se in un singolo fotogramma cambia una porzione dell'immagine superiore a tale frazione, il rilevatore non restituirà alcun riquadro di movimento e si ricalibrerà immediatamente. Ciò consente di risparmiare risorse della CPU e ridurre i falsi positivi in caso di fulmini, temporali, ecc., ma potrebbe comportare la mancata rilevazione di eventi reali, come ad esempio il tracciamento automatico di un oggetto da parte di una telecamera PTZ. Il compromesso consiste nel scegliere se sacrificare alcuni megabyte di registrazioni o rivedere un paio di brevi video. Lasciare non impostato (Nessuno) per disabilitare questa funzione." }, "delta_alpha": { - "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento." + "description": "Fattore di fusione alfa utilizzato nel calcolo della differenza tra fotogrammi per il calcolo del movimento.", + "label": "Delta alfa" }, "frame_alpha": { - "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento." + "description": "Valore alfa utilizzato durante la fusione dei fotogrammi per la preelaborazione del movimento.", + "label": "Frame alfa" }, "frame_height": { "label": "Altezza del frame", @@ -672,7 +869,8 @@ "description": "Secondi di attesa dopo l'ultimo movimento prima di pubblicare uno stato 'off' MQTT." }, "enabled_in_config": { - "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale." + "description": "Indica se il rilevamento del movimento era abilitato nella configurazione statica originale.", + "label": "Stato movimento originale" }, "raw_mask": { "label": "Maschera grezza" @@ -710,11 +908,11 @@ }, "min_score": { "label": "Confidenza minima", - "description": "Livello minimo di confidenza del rilevamento per singolo fotogramma richiesto affinché l'oggetto venga conteggiato." + "description": "Livello minimo di confidenza nel rilevamento in un singolo fotogramma richiesto affinché l'oggetto venga conteggiato." }, "mask": { "label": "Maschera filtro", - "description": "Coordinate del poligono che definisce l'area all'interno dell'inquadratura in cui si applica questo filtro." + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." }, "raw_mask": { "label": "Maschera grezza" @@ -722,40 +920,42 @@ }, "mask": { "label": "Maschera oggetto", - "description": "Poligono di maschera utilizzato per impedire il rilevamento di oggetti in aree specificate." + "description": "Poligono di mascheramento utilizzato per impedire il rilevamento di oggetti in aree specifiche." }, "genai": { "label": "Configurazione degli oggetti GenAI", "description": "Opzioni GenAI per la descrizione degli oggetti tracciati e l'invio dei fotogrammi per la generazione.", "enabled": { "label": "Abilita GenAI", - "description": "Abilita di default la generazione tramite GenAI delle descrizioni degli oggetti monitorati." + "description": "Abilita in modo predefinito la generazione di descrizioni da parte di GenAI per gli oggetti tracciati." }, "use_snapshot": { - "label": "Usa istantanee", + "label": "Utilizza istantanee", "description": "Utilizza le istantanee degli oggetti anziché le miniature per la generazione di descrizioni con GenAI." }, "prompt": { - "description": "Modello di prompt predefinito utilizzato per la generazione di descrizioni con GenAI." + "description": "Modello di richiesta predefinito utilizzato per la generazione di descrizioni con GenAI.", + "label": "Richiesta di didascalia" }, "object_prompts": { - "description": "Prompt specifici per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche." + "description": "Richieste specifiche per ogni oggetto per personalizzare i risultati di GenAI in base a etichette specifiche.", + "label": "Richiesta di oggetto" }, "objects": { "label": "Oggetti GenAI", - "description": "Elenco delle etichette degli oggetti da inviare a GenAI per impostazione predefinita." + "description": "Elenco di etichette degli oggetti da inviare a GenAI per impostazione predefinita." }, "required_zones": { - "label": "Zone obbligatorie", - "description": "Zone che devono essere inserite affinché gli oggetti possano essere utilizzati per la generazione di descrizioni tramite GenAI." + "label": "Zone richieste", + "description": "Zone che devono essere inserite affinché gli oggetti siano idonei alla generazione della descrizione GenAI." }, "debug_save_thumbnails": { "label": "Salva miniature", - "description": "Salva le miniature inviate a GenAI per il debug e la revisione." + "description": "Salva le miniature inviate a GenAI per la correzione e la revisione." }, "send_triggers": { - "label": "Attivatori GenAI", - "description": "Definisce quando i frame devono essere inviati a GenAI (al termine, dopo gli aggiornamenti, ecc.).", + "label": "Attivazioni GenAI", + "description": "Definisce quando i fotogrammi devono essere inviati a GenAI (alla fine, dopo gli aggiornamenti, ecc.).", "tracked_object_end": { "label": "Invia alla fine", "description": "Invia una richiesta a GenAI quando l'oggetto tracciato termina." @@ -776,6 +976,24 @@ "filters_attribute": { "raw_mask": { "label": "Maschera grezza" + }, + "min_ratio": { + "label": "Rapporto di aspetto minimo", + "description": "Rapporto minimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "max_ratio": { + "label": "Rapporto di aspetto massimo", + "description": "Rapporto massimo tra larghezza e altezza richiesto affinché il riquadro di delimitazione sia valido." + }, + "threshold": { + "label": "Soglia di confidenza" + }, + "min_score": { + "label": "Confidenza minima" + }, + "mask": { + "label": "Maschera filtro", + "description": "Coordinate del poligono che definiscono la posizione di applicazione di questo filtro all'interno dell'inquadratura." } } }, @@ -795,7 +1013,39 @@ } }, "timestamp_style": { - "label": "Stile orario" + "label": "Stile orario", + "position": { + "label": "Posizione timestamp", + "description": "Posizione del timestamp sull'immagine (tl/tr/bl/br)." + }, + "format": { + "label": "Formato timestamp", + "description": "Stringa di formato data/ora utilizzata per i timestamp (codici di formato datetime di Python)." + }, + "color": { + "label": "Colore timestamp", + "description": "Valori di colore RGB per il testo del timestamp (tutti i valori da 0 a 255).", + "red": { + "label": "Rosso", + "description": "Componente rosso (0-255) per il colore del timestamp." + }, + "green": { + "label": "Verde", + "description": "Componente verde (0-255) per il colore del timestamp." + }, + "blue": { + "label": "Blu", + "description": "Componente blu (0-255) per il colore del timestamp." + } + }, + "thickness": { + "label": "Spessore timestamp", + "description": "Spessore della linea del testo del timestamp." + }, + "effect": { + "label": "Effetto timestamp", + "description": "Effetto visivo per il testo del timestamp (none, solid, shadow)." + } }, "database": { "label": "Database", @@ -824,7 +1074,7 @@ "description": "Indirizzo email utilizzato per le notifiche push o richiesto da alcuni fornitori di servizi di notifica." }, "cooldown": { - "label": "Periodo di raffreddamento", + "label": "Tempo di attesa tra notifiche", "description": "Tempo di attesa (in secondi) tra le notifiche per evitare di inviare spam ai destinatari." }, "enabled_in_config": { @@ -857,10 +1107,44 @@ } }, "tls": { - "label": "TLS" + "label": "TLS", + "description": "Impostazioni TLS per gli endpoint web di Frigate (porta 8971).", + "enabled": { + "label": "Abilita TLS", + "description": "Abilita il protocollo TLS per l'interfaccia utente web e le API di Frigate sulla porta TLS configurata." + } }, "telemetry": { - "label": "Telemetria" + "label": "Telemetria", + "description": "Opzioni di telemetria e statistiche di sistema, incluso il monitoraggio della GPU e della larghezza di banda di rete.", + "network_interfaces": { + "label": "Interfacce di rete", + "description": "Elenco dei prefissi dei nomi delle interfacce di rete da monitorare per le statistiche sulla larghezza di banda." + }, + "stats": { + "label": "Statistiche di sistema", + "description": "Opzioni per abilitare/disabilitare la raccolta di varie statistiche di sistema e della GPU.", + "amd_gpu_stats": { + "label": "Statistiche GPU AMD", + "description": "Abilita la raccolta delle statistiche della GPU AMD, se presente una GPU AMD." + }, + "intel_gpu_stats": { + "label": "Statistiche GPU Intel", + "description": "Abilita la raccolta delle statistiche della GPU Intel, se presente una GPU Intel." + }, + "network_bandwidth": { + "label": "Larghezza di banda di rete", + "description": "Abilita il monitoraggio della larghezza di banda di rete per singolo processo per i processi ffmpeg della telecamera e per i rilevatori (richiede capability specifiche)." + }, + "intel_gpu_device": { + "label": "Dispositivo GPU Intel", + "description": "Indirizzo bus PCI o percorso del dispositivo DRM (es. /dev/dri/card1) utilizzato per associare le statistiche della GPU Intel a un dispositivo specifico quando ne sono presenti molteplici." + } + }, + "version_check": { + "label": "Controllo versione", + "description": "Abilita un controllo in uscita per rilevare se è disponibile una versione più recente di Frigate." + } }, "birdseye": { "label": "Birdseye", @@ -887,7 +1171,7 @@ "label": "Attivatori", "friendly_name": { "label": "Nome descrittivo", - "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo innesco." + "description": "Nome descrittivo opzionale visualizzato nell'interfaccia utente per questo attivatore." }, "description": "Azioni e criteri di corrispondenza per gli attivatori della ricerca semantica specifici della telecamera.", "enabled": { diff --git a/web/public/locales/it/views/explore.json b/web/public/locales/it/views/explore.json index e01329aada..e08626bfa0 100644 --- a/web/public/locales/it/views/explore.json +++ b/web/public/locales/it/views/explore.json @@ -202,8 +202,8 @@ "aria": "Invia a Frigate Plus" }, "addTrigger": { - "label": "Aggiungi innesco", - "aria": "Aggiungi un innesco per questo oggetto tracciato" + "label": "Aggiungi attivatore", + "aria": "Aggiungi un attivatore per questo oggetto tracciato" }, "audioTranscription": { "label": "Trascrivere", diff --git a/web/public/locales/it/views/live.json b/web/public/locales/it/views/live.json index 697a676820..ac9fc12099 100644 --- a/web/public/locales/it/views/live.json +++ b/web/public/locales/it/views/live.json @@ -9,7 +9,7 @@ "disable": "Disabilita audio bidirezionale" }, "snapshots": { - "enable": "Abilita istantanee", + "enable": "Abilita Istantanee", "disable": "Disabilita istantanee" }, "manualRecording": { diff --git a/web/public/locales/it/views/settings.json b/web/public/locales/it/views/settings.json index 5c40cc9601..9ef1f0f295 100644 --- a/web/public/locales/it/views/settings.json +++ b/web/public/locales/it/views/settings.json @@ -212,7 +212,7 @@ "type": { "zone": "zona", "motion_mask": "maschera di movimento", - "object_mask": "maschera di oggetto" + "object_mask": "maschera oggetto" }, "revertOverride": { "title": "Ripristina la configurazione di base", @@ -447,7 +447,7 @@ "users": "Utenti", "frigateplus": "Frigate+", "enrichments": "Miglioramenti", - "triggers": "Inneschi", + "triggers": "Attivatori", "roles": "Ruoli", "cameraManagement": "Gestione della telecamera", "cameraReview": "Revisiona", @@ -896,22 +896,22 @@ "restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)" }, "triggers": { - "documentTitle": "Inneschi", + "documentTitle": "Attivatori", "management": { - "title": "Inneschi", - "desc": "Gestisci gli inneschi per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." + "title": "Attivatori", + "desc": "Gestisci gli attivatori per {{camera}}. Utilizza il tipo miniatura per attivare miniature simili all'oggetto tracciato selezionato e il tipo descrizione per attivare descrizioni simili al testo specificato." }, - "addTrigger": "Aggiungi innesco", + "addTrigger": "Aggiungi attivatore", "table": { "name": "Nome", "type": "Tipo", "content": "Contenuto", "threshold": "Soglia", "actions": "Azioni", - "noTriggers": "Nessun innesco configurato per questa telecamera.", + "noTriggers": "Nessun attivatore configurato per questa telecamera.", "edit": "Modifica", - "deleteTrigger": "Elimina innesco", - "lastTriggered": "Ultimo innesco" + "deleteTrigger": "Elimina attivatore", + "lastTriggered": "Ultimo attivatore" }, "type": { "thumbnail": "Miniatura", @@ -925,34 +925,34 @@ }, "dialog": { "createTrigger": { - "title": "Crea innesco", - "desc": "Crea un innesco per la telecamera {{camera}}" + "title": "Crea attivatore", + "desc": "Crea un attivatore per la telecamera {{camera}}" }, "editTrigger": { - "title": "Modifica innesco", - "desc": "Modifica le impostazioni per l'innesco della telecamera {{camera}}" + "title": "Modifica attivatore", + "desc": "Modifica le impostazioni per l'attivatore della telecamera {{camera}}" }, "deleteTrigger": { - "title": "Elimina innesco", - "desc": "Vuoi davvero eliminare l'innesco {{triggerName}}? Questa azione non può essere annullata." + "title": "Elimina attivatore", + "desc": "Vuoi davvero eliminare l'attivatore {{triggerName}}? Questa azione non può essere annullata." }, "form": { "name": { "title": "Nome", - "placeholder": "Assegna un nome a questo innesco", + "placeholder": "Assegna un nome a questo attivatore", "error": { "minLength": "Il campo deve contenere almeno 2 caratteri.", "invalidCharacters": "Il campo può contenere solo lettere, numeri, caratteri di sottolineatura e trattini.", - "alreadyExists": "Per questa telecamera esiste già un innesco con questo nome." + "alreadyExists": "Per questa telecamera esiste già un attivatore con questo nome." }, - "description": "Inserisci un nome o una descrizione univoca per identificare questo innesco" + "description": "Inserisci un nome o una descrizione univoca per identificare questo attivatore" }, "enabled": { - "description": "Abilita o disabilita questo innesco" + "description": "Abilita o disabilita questo attivatore" }, "type": { "title": "Tipo", - "placeholder": "Seleziona il tipo di innesco", + "placeholder": "Seleziona il tipo di attivatore", "description": "Si attiva quando viene rilevata una descrizione di un oggetto simile tracciato", "thumbnail": "Attiva quando viene rilevata una miniatura di un oggetto simile tracciato" }, @@ -960,7 +960,7 @@ "title": "Contenuto", "imagePlaceholder": "Seleziona una miniatura", "textPlaceholder": "Inserisci il contenuto del testo", - "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un innesco dal menu.", + "imageDesc": "Vengono visualizzate solo le 100 miniature più recenti. Se non riesci a trovare la miniatura desiderata, controlla gli oggetti precedenti in Esplora e imposta un attivatore dal menu.", "textDesc": "Inserisci il testo per attivare questa azione quando viene rilevata una descrizione simile dell'oggetto tracciato.", "error": { "required": "Il contenuto è obbligatorio." @@ -972,11 +972,11 @@ "min": "La soglia deve essere almeno 0", "max": "La soglia deve essere al massimo 1" }, - "desc": "Imposta la soglia di similarità per questo innesco. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'innesco." + "desc": "Imposta la soglia di similarità per questo attivatore. Una soglia più alta indica che è necessaria una corrispondenza più vicina per attivare l'attivatore." }, "actions": { "title": "Azioni", - "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli inneschi. Le sottoetichette aggiungono il nome dell'innesco all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.", + "desc": "Per impostazione predefinita, Frigate invia un messaggio MQTT per tutti gli attivatori. Le sottoetichette aggiungono il nome dell'attivatore all'etichetta dell'oggetto. Gli attributi sono metadati ricercabili, memorizzati separatamente nei metadati dell'oggetto tracciato.", "error": { "min": "È necessario selezionare almeno un'azione." } @@ -990,30 +990,30 @@ }, "toast": { "success": { - "createTrigger": "L'innesco {{name}} è stato creato correttamente.", - "updateTrigger": "L'innesco {{name}} è stato aggiornato correttamente.", - "deleteTrigger": "L'innesco {{name}} è stato eliminato correttamente." + "createTrigger": "L'attivatore {{name}} è stato creato correttamente.", + "updateTrigger": "L'attivatore {{name}} è stato aggiornato correttamente.", + "deleteTrigger": "L'attivatore {{name}} è stato eliminato correttamente." }, "error": { - "createTriggerFailed": "Impossibile creare l'innesco: {{errorMessage}}", - "updateTriggerFailed": "Impossibile aggiornare l'innesco: {{errorMessage}}", - "deleteTriggerFailed": "Impossibile eliminare l'innesco: {{errorMessage}}" + "createTriggerFailed": "Impossibile creare l'attivatore: {{errorMessage}}", + "updateTriggerFailed": "Impossibile aggiornare l'attivatore: {{errorMessage}}", + "deleteTriggerFailed": "Impossibile eliminare l'attivatore: {{errorMessage}}" } }, "semanticSearch": { "title": "La ricerca semantica è disabilitata", - "desc": "Per utilizzare gli inneschi, è necessario abilitare la ricerca semantica." + "desc": "Per utilizzare gli attivatori, è necessario abilitare la ricerca semantica." }, "wizard": { - "title": "Crea innesco", + "title": "Crea attivatore", "step1": { - "description": "Configura le impostazioni di base per il tuo innesco." + "description": "Configura le impostazioni di base per il tuo attivatore." }, "step2": { "description": "Imposta il contenuto che attiverà questa azione." }, "step3": { - "description": "Configura la soglia e le azioni per questo innesco." + "description": "Configura la soglia e le azioni per questo attivatore." }, "steps": { "nameAndType": "Nome e tipo", @@ -2030,10 +2030,10 @@ "desc": "La griglia di regioni è un algoritmo di ottimizzazione che apprende dove gli oggetti di diverse dimensioni appaiono tipicamente nel campo visivo di ciascuna telecamera. Frigate utilizza questi dati per dimensionare in modo efficiente le regioni di rilevamento. La griglia viene creata automaticamente nel tempo a partire dai dati degli oggetti tracciati.", "clear": "Pulisci griglia di regioni", "clearConfirmTitle": "Pulisci griglia di regioni", - "clearConfirmDesc": "La pulizia della griglia di regioni non è consigliata a meno che non si sia recentemente modificato il modello del rilevatore o la posizione fisica della telecamera, riscontrando problemi di tracciamento degli oggetti. La griglia verrà ricostruita automaticamente nel tempo man mano che gli oggetti vengono tracciati. Per rendere effettive le modifiche è necessario riavviare Frigate.", - "clearSuccess": "Griglia di regioni pulita con successo", - "clearError": "Impossibile pulire la griglia di regioni", - "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia di regioni abbiano effetto" + "clearConfirmDesc": "La pulizia della griglia delle regioni non è consigliata, a meno che non sia stata modificata di recente la dimensione del modello del rilevatore, oppure sia stata cambiata la posizione fisica della telecamera e si riscontrino problemi di tracciamento degli oggetti. La griglia verrà ricostruita automaticamente nel tempo man mano che gli oggetti vengono tracciati. È richiesto un riavvio di Frigate affinché le modifiche abbiano effetto.", + "clearSuccess": "Griglia delle regioni pulita con successo", + "clearError": "Impossibile pulire la griglia delle regioni", + "restartRequired": "È necessario riavviare il sistema affinché le modifiche alla griglia delle regioni abbiano effetto" } }, "retainMode": { diff --git a/web/public/locales/nb-NO/common.json b/web/public/locales/nb-NO/common.json index 00fb832e3f..efe695371a 100644 --- a/web/public/locales/nb-NO/common.json +++ b/web/public/locales/nb-NO/common.json @@ -217,7 +217,8 @@ "id": "Bahasa Indonesia (Indonesisk)", "ur": "اردو (Urdu)", "hr": "Hrvatski (Kroatisk)", - "bs": "Bosanski (Bosnisk)" + "bs": "Bosanski (Bosnisk)", + "zhHant": "繁體中文 (Tradisjonell kinesisk)" }, "appearance": "Utseende", "darkMode": { @@ -326,5 +327,8 @@ "separatorWithSpace": ", " }, "validation_errors": "Valideringsfeil", - "no_items": "Ingen elementer" + "no_items": "Ingen elementer", + "credentialField": { + "savedPlaceholder": "Lagret – la stå tomt for å beholde gjeldende" + } } diff --git a/web/public/locales/nb-NO/components/camera.json b/web/public/locales/nb-NO/components/camera.json index 601da4bc1a..60d318a4ad 100644 --- a/web/public/locales/nb-NO/components/camera.json +++ b/web/public/locales/nb-NO/components/camera.json @@ -68,7 +68,10 @@ "desc": "Velg kameraer for denne gruppen." }, "icon": "Ikon", - "success": "Kameragruppen ({{name}}) er lagret." + "success": "Kameragruppen ({{name}}) er lagret.", + "showAll": "Vis alle kameragrupper", + "showLess": "Vis mindre", + "editGroups": "Rediger kameragrupper" }, "debug": { "options": { diff --git a/web/public/locales/nb-NO/components/dialog.json b/web/public/locales/nb-NO/components/dialog.json index ebe531b4c4..cdb09308a3 100644 --- a/web/public/locales/nb-NO/components/dialog.json +++ b/web/public/locales/nb-NO/components/dialog.json @@ -34,6 +34,9 @@ "ask_an": "Er dette objekt en {{label}}?", "ask_a": "Er dette objektet en {{label}}?", "ask_full": "Er dette objekt en {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Kunne ikke sende til Frigate+. Vennligst sjekk nettverksforbindelsen din og prøv igjen." } } }, @@ -112,7 +115,14 @@ "detectionCount_one": "1 sporet objekt", "detectionCount_other": "{{count}} sporede objekter", "exportButton_one": "Eksporter 1 kamera", - "exportButton_other": "Eksporter {{count}} kameraer" + "exportButton_other": "Eksporter {{count}} kameraer", + "searchOrSelectGroup": "Søk, eller velg en kameragruppe...", + "selectAll": "Velg alle kameraer", + "clearSelection": "Nullstill valg", + "selectWithActivity": "Kameraer med sporede objekter", + "selectGroup": "Velg gruppe", + "noMatchingCameras": "Ingen kameraer samsvarer med søket ditt", + "selectedCount": "{{selected}} / {{total}} valgt" }, "multi": { "description": "Eksporter hver valgte inspeksjon. Alle eksporter vil bli gruppert under én sak.", diff --git a/web/public/locales/nb-NO/components/player.json b/web/public/locales/nb-NO/components/player.json index 56fe61e9ab..31e8e28b4a 100644 --- a/web/public/locales/nb-NO/components/player.json +++ b/web/public/locales/nb-NO/components/player.json @@ -48,5 +48,6 @@ "error": { "submitFrigatePlusFailed": "Kunne ikke sende bildet til Frigate+" } - } + }, + "cameraOff": "Kameraet er av" } diff --git a/web/public/locales/nb-NO/config/cameras.json b/web/public/locales/nb-NO/config/cameras.json index da2f22cfe0..5f8c818459 100644 --- a/web/public/locales/nb-NO/config/cameras.json +++ b/web/public/locales/nb-NO/config/cameras.json @@ -67,7 +67,7 @@ }, "listen": { "label": "Lyttetyper", - "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, skrik, tale, rop)." + "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)." }, "filters": { "label": "Lydfiltre", @@ -164,11 +164,11 @@ } }, "ffmpeg": { - "label": "FFmpeg", + "label": "Strømmer (FFmpeg)", "description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.", "path": { "label": "FFmpeg-sti", - "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"5.0\" eller \"7.0\")." + "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")." }, "global_args": { "label": "Globale FFmpeg-argumenter", @@ -484,6 +484,9 @@ "max_concurrent": { "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", "label": "Maksimalt antall samtidige eksporter" + }, + "chapters": { + "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak" } }, "preview": { @@ -674,7 +677,7 @@ "label": "Effekt for tidsstempel", "description": "Visuell effekt for tidsstempelteksten (none, solid, shadow)." }, - "description": "Stilalternativer for tidsstempler i strømmen, brukt på opptak og stillbilder." + "description": "Stilalternativer for tidsstempler brukt på stillbilder og i Feilsøkingsvisning." }, "audio_transcription": { "label": "Lydtranskripsjon", @@ -870,6 +873,10 @@ "order": { "description": "Numerisk rekkefølge for sortering av kameraet i grensesnittet; høyere tall vises senere.", "label": "Rekkefølge i UI" + }, + "review": { + "description": "Velg om dette kameraet skal være synlig i inspeksjonen (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning).", + "label": "Vis i inspeksjon" } }, "webui_url": { diff --git a/web/public/locales/nb-NO/config/global.json b/web/public/locales/nb-NO/config/global.json index 9b288f47f3..5f50981a88 100644 --- a/web/public/locales/nb-NO/config/global.json +++ b/web/public/locales/nb-NO/config/global.json @@ -212,7 +212,7 @@ }, "default_role": { "label": "Standardrolle", - "description": "Standardrolle tildelt proxy-autentiserte brukere når ingen rolletilordning gjelder (admin eller viewer)." + "description": "Standardrolle tildelt proxy-autentiserte brukere når ingen rolletilordning gjelder." }, "separator": { "label": "Skilletegn", @@ -555,7 +555,7 @@ }, "listen": { "label": "Lyttetyper", - "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, skrik, tale, rop)." + "description": "Liste over typer lydhendelser som skal detekteres (f.eks. bjeff, brannalarm, tale, rop)." }, "filters": { "label": "Lydfiltre", @@ -691,7 +691,7 @@ "description": "FFmpeg-innstillinger, inkludert sti til binærfil, argumenter, alternativer for maskinvareakselerasjon og utdata-argumenter per rolle.", "path": { "label": "FFmpeg-sti", - "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"5.0\" eller \"7.0\")." + "description": "Sti til FFmpeg-binærfilen som skal brukes, eller et versjonsalias (\"7.0\" eller \"8.0\")." }, "global_args": { "label": "Globale FFmpeg-argumenter", @@ -1043,6 +1043,9 @@ "max_concurrent": { "description": "Maksimalt antall eksportjobber som kan behandles samtidig.", "label": "Maksimalt antall samtidige eksporter" + }, + "chapters": { + "label": "Kapittelmetadata som skal bygges inn i eksporterte opptak" } }, "preview": { @@ -1554,6 +1557,10 @@ "dashboard": { "label": "Vis i grensesnitt", "description": "Velg om dette kameraet skal være synlig i Frigate-grensesnittet. Deaktivering krever manuell redigering av konfigurasjonen for å vise kameraet igjen." + }, + "review": { + "label": "Vis i inspeksjon", + "description": "Velg om dette kameraet skal være synlig i inspeksjon (inspeksjonssiden og dens kamerafilter, bevegelsesinspeksjon og historikkvisning)." } }, "onvif": { diff --git a/web/public/locales/nb-NO/config/validation.json b/web/public/locales/nb-NO/config/validation.json index e9e34a202c..afc1e4b332 100644 --- a/web/public/locales/nb-NO/config/validation.json +++ b/web/public/locales/nb-NO/config/validation.json @@ -28,5 +28,8 @@ } }, "minLength": "Må være minst {{limit}} tegn", - "maxLength": "Må være maks {{limit}} tegn" + "maxLength": "Må være maks {{limit}} tegn", + "detect": { + "dimensionMustBeEven": "Må være et partall." + } } diff --git a/web/public/locales/nb-NO/views/chat.json b/web/public/locales/nb-NO/views/chat.json index 7d42966fd4..fcef10748a 100644 --- a/web/public/locales/nb-NO/views/chat.json +++ b/web/public/locales/nb-NO/views/chat.json @@ -65,5 +65,8 @@ "active": "Resonnerer…", "show": "Vis resonnering", "hide": "Skjul resonnering" + }, + "thinking": { + "toggle": "Aktiver/deaktiver tenking" } } diff --git a/web/public/locales/nb-NO/views/live.json b/web/public/locales/nb-NO/views/live.json index da219820f5..4ba80b0dfa 100644 --- a/web/public/locales/nb-NO/views/live.json +++ b/web/public/locales/nb-NO/views/live.json @@ -50,7 +50,9 @@ }, "camera": { "enable": "Aktiver kamera", - "disable": "Deaktiver kamera" + "disable": "Deaktiver kamera", + "turnOn": "Slå kamera på", + "turnOff": "Skå kamera av" }, "snapshots": { "enable": "Aktiver stillbilder", @@ -169,7 +171,8 @@ "snapshots": "Stillbilder", "audioDetection": "Lydregistrering", "autotracking": "Automatisk sporing", - "transcription": "Lydtranskripsjon" + "transcription": "Lydtranskripsjon", + "camera": "Kamera" }, "transcription": { "enable": "Aktiver direkte lydtranskripsjon", diff --git a/web/public/locales/nb-NO/views/motionSearch.json b/web/public/locales/nb-NO/views/motionSearch.json index c8fdb7c873..79e2a6d312 100644 --- a/web/public/locales/nb-NO/views/motionSearch.json +++ b/web/public/locales/nb-NO/views/motionSearch.json @@ -24,7 +24,9 @@ "points_one": "{{count}} punkt", "points_other": "{{count}} punkter", "undo": "Angre siste punkt", - "reset": "Tilbakestill polygon" + "reset": "Tilbakestill polygon", + "moveMode": "Flytt", + "drawMode": "Tegn" }, "motionHeatmapLabel": "Varmekart for bevegelse", "dialog": { @@ -40,11 +42,11 @@ "settings": { "title": "Søkeinnstillinger", "parallelMode": "Parallellmodus", - "parallelModeDesc": "Skann flere opptakssegmenter samtidig (raskere, men betydelig mer CPU-intensivt)", + "parallelModeDesc": "Skann flere opptaksintervaller samtidig (raskere, men krever mer dekodingsressurser)", "threshold": "Følsomhetsterskel", "thresholdDesc": "Lavere verdier detekterer mindre endringer (1–255)", "minArea": "Minimum endringsområde", - "minAreaDesc": "Minimum prosentandel av interesseområdet som må endres for å anses som betydelig", + "minAreaDesc": "Minimumsstørrelse for et enkelt bevegelsesområde, som en prosentandel av interesseområdet", "frameSkip": "Bilde-sprang", "frameSkipDesc": "Behandle hvert N-te bilde. Sett denne til kameraets bildefrekvens for å behandle ett bilde i sekundet (f.eks. 5 for et 5 FPS-kamera, 30 for et 30 FPS-kamera). Høyere verdier vil være raskere, men kan gå glipp av korte bevegelseshendelser.", "maxResults": "Maksimalt antall resultater", @@ -70,6 +72,9 @@ "framesDecoded": "Bilder dekodet", "wallTime": "Søketid", "segmentErrors": "Segmentfeil", - "seconds": "{{seconds}}s" - } + "seconds": "{{seconds}}s", + "scanSummary": "{{segments}} segmenter · {{time}}", + "minutesSeconds": "{{minutes}}m {{seconds}}s" + }, + "scanning": "Skanner {{time}}" } diff --git a/web/public/locales/nb-NO/views/settings.json b/web/public/locales/nb-NO/views/settings.json index fac229142e..73de4de4fd 100644 --- a/web/public/locales/nb-NO/views/settings.json +++ b/web/public/locales/nb-NO/views/settings.json @@ -31,7 +31,7 @@ "notifications": "Meldingsvarsler", "enrichments": "Utvidelser", "triggers": "Utløsere", - "cameraManagement": "Administrasjon", + "cameraManagement": "Kamerabehandling", "cameraReview": "Inspeksjon", "roles": "Roller", "profiles": "Profiler", @@ -46,7 +46,7 @@ "systemDetectionModel": "Deteksjonsmodell", "cameraLivePlayback": "Direkteavspilling", "globalLivePlayback": "Direkteavspilling", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Strømmer (FFmpeg)", "globalFfmpeg": "FFmpeg", "systemFfmpeg": "FFmpeg", "cameraBirdseye": "Fugleperspektiv", @@ -723,7 +723,8 @@ "notificationUnavailable": { "documentation": "Se dokumentasjonen", "title": "Meldingsvarsler utilgjengelig", - "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler." + "desc": "Nettleser push-varsler krever et sikkert miljø (https://…). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler.", + "descPwa": "På iOS er web-pushvarsler kun tilgjengelig når Frigate er installert på hjemskjermen din. Åpne Del-menyen, velg Legg til på hjemskjermen, og åpne deretter Frigate fra det nye ikonet for å registrere denne enheten for varsler." }, "email": { "title": "E-post", @@ -1136,7 +1137,7 @@ "brands": { "reolink-rtsp": "Reolink RTSP anbefales ikke. Aktiver HTTP i kameraets fastvare-innstillinger og start kameraveiviseren på nytt." }, - "customUrlRtspRequired": "Egendefinerte URL-er må begynne med \"rtsp://\". Manuell konfigurering kreves for kamera­strømmer som ikke bruker RTSP." + "customUrlRtspRequired": "Egendefinerte URL-er må begynne med «rtsp://» eller «rtsps://». Manuell konfigurasjon kreves for kamerastrømmer som ikke bruker RTSP." }, "docs": { "reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras" @@ -1364,10 +1365,10 @@ "selectCamera": "Velg et kamera", "backToSettings": "Tilbake til kamerainnstillinger", "streams": { - "title": "Aktiver / Deaktiver kameraer", + "title": "Kamerastatus og detaljer", "desc": "Midlertidig deaktiver et kamera til Frigate startes på nytt. Deaktivering av et kamera stopper Frigates behandling av dette kameraets strømmer fullstendig. Deteksjon, opptak og feilsøking vil være utilgjengelig.
Merk: Dette deaktiverer ikke go2rtc-restrømming.", "disableDesc": "Aktiver et kamera som for øyeblikket ikke er synlig i grensesnittet og deaktivert i konfigurasjonen. En omstart av Frigate kreves etter aktivering.", - "enableSuccess": "Aktiverte {{cameraName}} i konfigurasjonen. Start Frigate på nytt for å ta i bruk endringene.", + "enableSuccess": "Aktiverte {{cameraName}}. Start Frigate på nytt for å ta i bruk.", "enableLabel": "Aktiverte kameraer", "enableDesc": "Deaktiver et aktivert kamera midlertidig frem til Frigate starter på nytt. Deaktivering av et kamera stopper all prosessering av kameraets strømmer. Deteksjon, opptak og feilsøking vil være utilgjengelig.
Merk: Dette deaktiverer ikke videreformidling (restream) i go2rtc.

Dra i feltet for å endre rekkefølgen på kameraene slik de vises i grensesnittet. Rekkefølgen på de aktiverte kameraene vil gjenspeiles i hele grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.", "disableLabel": "Deaktiverte kameraer", @@ -1379,7 +1380,30 @@ }, "reorderHandle": "Dra for å endre rekkefølge", "saving": "Lagrer…", - "saved": "Lagret" + "saved": "Lagret", + "label": "Kamerastatus", + "description": "Angi driftsstatus for hvert kamera.

: Strømmer behandles normalt.
Av: Setter behandlingen midlertidig på pause. Vedvarer ikke etter omstart av Frigate.
Deaktivert: Stopper behandlingen og lagrer endringen i konfigurasjonen din. En omstart kreves for å aktivere et deaktivert kamera igjen.

Merk: Deaktivering påvirker ikke videreformidling (restream) i go2rtc.

Dra i ikonet for å endre rekkefølgen på aktive kameraer slik de vises i grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.", + "disabledSubheading": "Deaktivert i konfigurasjonen", + "status": { + "on": "På", + "off": "Av", + "disabled": "Deaktivert" + }, + "disableSuccess": "Deaktiverte {{cameraName}} og lagret i konfigurasjonen.", + "details": { + "edit": "Rediger kameradetaljer", + "title": "Rediger kameradetaljer", + "description": "Oppdater visningsnavn, ekstern URL og synlighet brukt for dette kameraet i Frigate-grensesnittet.", + "friendlyNameLabel": "Visningsnavn", + "friendlyNameHelp": "Vennlig navn som vises for dette kameraet i Frigate-grensesnittet. La stå tomt for å bruke kamera-ID.", + "webuiUrlLabel": "URL til kameraets webgrensesnitt", + "webuiUrlHelp": "URL for å besøke kameraets webgrensesnitt direkte fra feilsøkingsvisningen (Debug). La stå tomt for å deaktivere lenken.", + "webuiUrlInvalid": "Må være en gyldig URL (f.eks. https://example.com).", + "dashboardLabel": "Vis på Live-dashbordet", + "dashboardHelp": "Vis dette kameraet på Live-dashbordet.", + "reviewLabel": "Vis i Inspeksjons-modus", + "reviewHelp": "Vis dette kameraet i Inspeksjon (Review), inkludert kamerafilteret, bevegelsesgjennomgang og historikkvisningen." + } }, "cameraConfig": { "add": "Legg til kamera", @@ -1414,9 +1438,11 @@ "enabled": "Aktivert", "inherit": "Arv", "disabled": "Deaktivert", - "description": "Konfigurer hvilke kameraer som er aktivert eller deaktivert når en profil aktiveres. Kameraer satt til \"Arv\" beholder sin opprinnelige status.", + "description": "Konfigurer hvilke kameraer som slås på eller av når en profil aktiveres. Kameraer satt til «Arv» beholder sin standardstatus.", "title": "Profiloverstyringer for kamera", - "selectLabel": "Velg profil" + "selectLabel": "Velg profil", + "on": "På", + "off": "Av" }, "deleteCameraDialog": { "confirmTitle": "Er du sikker?", @@ -1438,7 +1464,92 @@ "dedicatedLpr": "Dedikert LPR (lesing av kjennemerker)", "saveSuccess": "Kameratype oppdatert for {{cameraName}}. Start Frigate på nytt for å bruke endringene." }, - "description": "Legg til, rediger og slett kameraer. Kontroller hvilke kameraer som er aktivert og konfigurer overstyringer for hver profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon." + "description": "Legg til, rediger og slett kameraer, kontroller statusen til hvert kamera, og konfigurer overstyringer per profil og kameratype. For å konfigurere strømmer, deteksjon, bevegelse og andre kameraspesifikke innstillinger, velg den aktuelle seksjonen under Kamerakonfigurasjon.", + "clone": { + "sectionTitle": "Klon innstillinger", + "sectionDescription": "Kopier konfigurasjon fra ett kamera til et annet eller til et nytt kamera.", + "button": "Klon innstillinger", + "title": "Klon kamerainnstillinger", + "description": "Kopier et kameras konfigurasjon til ett eller flere andre kameraer eller til et nytt kamera. Identitet (navn, visningsnavn, URL til webgrensesnitt, visningsrekkefølge) kopieres ikke.", + "source": { + "label": "Kildekamera", + "placeholder": "Velg et kildekamera", + "required": "Kildekamera må velges" + }, + "target": { + "legend": "Mål", + "newRadio": "Nytt kamera", + "newNameLabel": "Kameranavn", + "newNamePlaceholder": "f.eks. bakdoer eller Bakdør", + "newNameRequired": "Kameranavn kreves", + "newNameInvalid": "Ugyldig kameranavn", + "newNameCollision": "Et kamera med dette navnet eksisterer allerede", + "newStreamsForced": "Strømmer kopieres alltid for et nytt kamera.", + "existingCamerasRadio": "Eksisterende kameraer", + "allCameras": "Alle kameraer", + "existingPlaceholder": "Velg minst ett kamera", + "existingDisabled": "Ingen andre kameraer å kopiere til" + }, + "categories": { + "legend": "Innstillinger som skal klones", + "description": "Velg hvilke innstillinger som skal kopieres fra kildekameraet.", + "selectAll": "Velg alle", + "selectNone": "Velg ingen", + "resetDefaults": "Tilbakestill til standardverdier", + "general": "Generelt", + "spatial": "Romlige innstillinger", + "streams": "Strømmer", + "spatialWarningTitle": "Uoverensstemmelse i oppløsning", + "spatialWarning": "Kildekameraet {{srcCamera}} sin deteksjonsoppløsning ({{srcWidth}}×{{srcHeight}}) er forskjellig fra: {{cameras}}. Polygoner vil kanskje ikke samsvare på disse kameraene. Disse er deaktivert som standard; aktiver for å kopiere som de er.", + "restartHint": "Omstart kreves", + "items": { + "record": "Opptak pågår", + "snapshots": "Øyeblikksbilder", + "review": "Inspeksjon", + "motion": "Bevegelsesdeteksjon", + "objects": "Objekter", + "audio": "Lyddeteksjon", + "audio_transcription": "Lydtranskripsjon", + "notifications": "Varsler", + "birdseye": "Fugleperspektiv", + "mqtt": "MQTT", + "timestamp_style": "Tidsstempelstil", + "onvif": "ONVIF", + "lpr": "Kjennemerke gjenkjenning", + "face_recognition": "Ansiktsgjenkjenning", + "semantic_search": "Semantisk søk", + "genai": "Generativ AI", + "type": "Kameratype (normal / dedikert LPR)", + "profiles": "Profiler", + "detect": "Deteksjonsdimensjoner", + "zones": "Soner", + "motion_mask": "Bevegelsesmasker", + "object_masks": "Objektmasker", + "ffmpeg_live": "Strømme-URL-er og roller" + } + }, + "footer": { + "restartNeeded": "Omstart vil kreves for enkelte endringer.", + "liveOnly": "Alle endringer vil trå i kraft direkte uten omstart.", + "submit": "Klon", + "submitting": "Kloner…", + "changeCount_one": "{{count}} endring vil bli brukt", + "changeCount_other": "{{count}} endringer vil bli brukt" + }, + "toast": { + "success": "Innstillinger kopiert til {{cameraName}}", + "successWithRestart": "Innstillinger kopiert til {{cameraName}}. Start Frigate på nytt for å ta i bruk alle endringer.", + "partialFailure": "{{successCount}} seksjoner ble tatt i bruk; «{{failedSection}}» feilet: {{errorMessage}}", + "partialFailureMulti": "Kopiert til {{successCount}} kamera(er); feilet for {{failed}}: {{errorMessage}}", + "newCameraPartialFailure": "Kameraet {{cameraName}} ble opprettet, men noen innstillinger kunne ikke kopieres: {{errorMessage}}", + "sourceMissing": "Kildekameraet eksisterer ikke lenger", + "submitError": "Kunne ikke klone kamera: {{errorMessage}}", + "successMulti_one": "Innstillinger kopiert til {{count}} kamera", + "successMulti_other": "Innstillinger kopiert til {{count}} kameraer", + "successMultiWithRestart_one": "Innstillinger kopiert til {{count}} kamera. Start Frigate på nytt for å aktivere alle endringene.", + "successMultiWithRestart_other": "Innstillinger kopiert til {{count}} kameraer. Start Frigate på nytt for å bruke alle endringene." + } + } }, "cameraReview": { "title": "Innstillinger for kamerainspeksjon", @@ -1659,18 +1770,58 @@ "addCustomLabel": "Legg til egendefinert etikett...", "restartRequiredField": "Omstart påkrevd", "cameraInputs": { - "itemTitle": "Strøm {{index}}" + "itemTitle": "Strøm {{index}}", + "sourceMode": { + "restream": "Videreformidling / Restream (go2rtc)", + "manual": "Manuell kildesti", + "go2rtcStreamLabel": "go2rtc-strøm", + "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm", + "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert", + "go2rtcStreamSearch": "Søk i strømmer...", + "availableStreams": "Tilgjengelige strømmer", + "noMatchingStreams": "Ingen matchende strømmer" + } }, "searchPlaceholder": "Søk...", "showAdvanced": "Vis avanserte innstillinger", "genaiModel": { - "placeholder": "Velg modell…", - "search": "Søk modeller…", - "noModels": "Ingen modeller tilgjengelig" + "placeholder": "Velg eller skriv inn en modell…", + "search": "Søk eller skriv inn en modell…", + "noModels": "Ingen modeller tilgjengelig", + "available": "Tilgjengelige modeller", + "useCustom": "Bruk «{{value}}»", + "refresh": "Oppdater modeller", + "probeFailed": "Kunne ikke hente modeller", + "fetchedModels": "Modelliste ble hentet" }, "knownPlates": { "platePlaceholder": "Kjennemerke nummer eller regex", "namePlaceholder": "f.eks konas bil" + }, + "liveStreams": { + "streamNameLabel": "Strømnavn", + "streamNamePlaceholder": "f.eks. Hovedstrøm HD", + "go2rtcStreamLabel": "go2rtc-strøm", + "go2rtcStreamPlaceholder": "Velg en go2rtc-strøm", + "go2rtcStreamSearch": "Søk eller skriv inn et strømnavn…", + "noGo2rtcStreams": "Ingen go2rtc-strømmer er konfigurert", + "availableStreams": "Tilgjengelige strømmer", + "useCustom": "Bruk «{{value}}»", + "addStream": "Legg til strøm" + }, + "semanticSearchModelSize": { + "notApplicable": "Ikke relevant for leverandører av generativ AI" + }, + "ptzPresets": { + "placeholder": "Velg eller skriv inn en forhåndsinnstilling...", + "search": "Søk eller skriv inn en forhåndsinnstilling...", + "noPresets": "Ingen forhåndsinnstillinger tilgjengelig", + "available": "Forhåndsinnstillinger for kamera", + "useCustom": "Bruk «{{value}}»" + }, + "defaultRole": { + "admin": "Administrator", + "viewer": "Seer (Viewer)" } }, "button": { @@ -1754,7 +1905,8 @@ "renameStreamDesc": "Skriv inn et nytt navn for denne strømmen. Endring av navn kan ødelegge for kameraer eller andre strømmer som refererer til den ved navn.", "deleteStream": "Slett strøm", "streamName": "Strømnavn", - "streamNumber": "Strøm {{index}}" + "streamNumber": "Strøm {{index}}", + "sourceNumber": "Kilde {{index}}" }, "profiles": { "active": "Aktiv", @@ -1816,7 +1968,13 @@ }, "detect": { "fpsGreaterThanFive": "Det anbefales ikke å sette FPS for deteksjon høyere enn 5. Høyere verdier kan føre til ytelsesproblemer uten å gi noen fordeler.", - "disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere." + "disabled": "Objektdeteksjon er deaktivert. Stillbilder, inspeksjonselementer og utvidelser som ansiktsgjenkjenning, lesing av kjennemerker og generativ AI vil ikke fungere.", + "resolutionShouldBeMultipleOfFour": "For best resultat bør deteksjonsbredde og -høyde være delelig med 4. Andre partall kan forårsake visuelle feil eller svak forvrengning i deteksjonsstrømmen.", + "aspectRatioMismatch": "Bredden og høyden du har oppgitt samsvarer ikke med sideforholdet til din nåværende deteksjonsoppløsning. Dette kan føre til et strukket eller forvrengt bilde.", + "maxFramesSet": "Å angi maksimalt antall bilder (max frames) overstyrer standard oppførsel og deaktiverer sporing av stasjonære objekter. Det er svært få situasjoner hvor dette er nødvendig, så bruk det med forsiktighet.", + "squareResolution": "En kvadratisk deteksjonsoppløsning er uvanlig. Deteksjonsbredden og -høyden bør samsvare med kameraets sideforhold (for eksempel 16:9), ikke dimensjonene til objektdeteksjonsmodellen. Et feilaktig sideforhold kan strekke bildet og redusere treffsikkerheten for deteksjon.", + "resolutionHigh": "Denne deteksjonsoppløsningen er høyere enn anbefalt og kan føre til økt ressursbruk uten å forbedre deteksjonsnøyaktigheten. En deteksjonsoppløsning på eller under 1080p anbefales for de fleste kameraer.", + "globalResolutionMultipleCameras": "En global deteksjonsoppløsning er satt samtidig som flere kameraer er konfigurert. Med mindre alle kameraer deler nøyaktig samme oppløsning og sideforhold, bør deteksjonsbredden og -høyden defineres per kamera for å matche hvert kameras opprinnelige sideforhold." }, "birdseye": { "objectsModeDetectDisabled": "Fugleperspektiv er satt til 'objekter'-modus, men objektdeteksjon er deaktivert for dette kameraet. Kameraet vil ikke vises i Fugleperspektiv." @@ -1843,6 +2001,16 @@ }, "semanticSearch": { "jinav2SmallModelSize": "Størrelsen \"liten\" med Jina V2-modellen har høyt minnebruk og beregningskostnad. Den \"store\" modellen med en dedikert GPU anbefales." + }, + "model": { + "optimizedFor320": "Frigate er optimalisert for en 320x320-modell, som er det beste valget for de fleste oppsett. En 640x640-modell er tregere og hjelper kun i spesifikke scenarier.", + "inputDimensionsNotDetectResolution": "Modellens inndata for bredde og høyde er dimensjonene til selve objektdeteksjonsmodellen, ikke kameraets deteksjonsoppløsning. De skal samsvare med dimensjonene til modellen du bruker – vanligvis en kvadratisk størrelse som 320x320 eller 640x640." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Manuelle argumenter for maskinvareakselerasjon anbefales ikke. Med mindre det foreligger et spesifikt behov, bør du velge forhåndsinnstillingen som samsvarer med maskinvaren din." + }, + "onvif": { + "autotrackingNoZones": "Autosporing krever minst én sone. Definer en sone for dette kameraet under Masker / Soner, og angi den deretter som en påkrevd sone nedenfor." } }, "maintenance": { diff --git a/web/public/locales/nb-NO/views/system.json b/web/public/locales/nb-NO/views/system.json index ef3ca18e1e..f849d700e7 100644 --- a/web/public/locales/nb-NO/views/system.json +++ b/web/public/locales/nb-NO/views/system.json @@ -170,7 +170,22 @@ "tips": { "title": "Kamerainformasjon" }, - "aspectRatio": "bildeforhold" + "aspectRatio": "bildeforhold", + "keyframes": { + "error": "Keyframe-intervallet (~{{seconds}}s) overskrider lengden på opptakssegmentet ({{segmentTime}}s). Enkelte segmenter kan mangle en keyframe, noe som ødelegger avspillingen. Deaktiver smart / +kodek'en på kameraet eller forkort keyframe-intervallet.", + "warning": "Spredte eller variable ke,yframes (lengste avstand ~{{seconds}}s), sannsynligvis på grunn av en smart-kodek (H.264+/H.265+). Dette er ikke anbefalt.", + "unknown": "Kunne ikke bestemme keyframe-intervallet.", + "ok": "Keyframes hvert ~{{seconds}}s, optimalt for opptak og avspilling.", + "gap": "Keyframe-intervall (min / snitt / maks):", + "keyframeCount": "Registrerte keyframes:", + "stillAnalyzing": "Analyserer keyframes fremdeles...", + "analyzing": "Analyserer keyframes... {{seconds}} sekunder gjenstår", + "title": "Keyframe-analyse", + "recordStream": "Opptaksstrøm:", + "observedDuration": "Observert varighet:", + "segmentLength": "Lengde på opptakssegment:", + "recordDisabled": "Opptak er deaktivert for dette kameraet." + } }, "framesAndDetections": "Bilder / Deteksjoner", "title": "Kameraer", diff --git a/web/public/locales/nl/common.json b/web/public/locales/nl/common.json index d47040c3bb..d9630b7928 100644 --- a/web/public/locales/nl/common.json +++ b/web/public/locales/nl/common.json @@ -216,7 +216,8 @@ "id": "Bahasa Indonesia (Indonesisch)", "ur": "اردو (Urdu)", "hr": "Hrvatski (Kroatisch)", - "zhHant": "繁體中文 (Traditioneel Chinees)" + "zhHant": "繁體中文 (Traditioneel Chinees)", + "bs": "Bosanksi (Bosnisch)" }, "darkMode": { "label": "Donkere modus", @@ -267,7 +268,10 @@ "anonymous": "anoniem" }, "classification": "Classificatie", - "profiles": "Profielen" + "profiles": "Profielen", + "actions": "Acties", + "features": "Functies", + "chat": "Chat" }, "toast": { "copyUrlToClipboard": "URL naar klembord gekopieerd.", @@ -276,7 +280,8 @@ "error": { "title": "Opslaan van configuratiewijzigingen mislukt: {{errorMessage}}", "noMessage": "Het opslaan van configuratiewijzigingen is mislukt" - } + }, + "success": "De configuratie aanpassingen zijn succesvol opgeslagen." } }, "role": { @@ -323,5 +328,7 @@ }, "credentialField": { "savedPlaceholder": "Opgeslagen - leeg laten om huidige te behouden" - } + }, + "no_items": "Geen items", + "validation_errors": "Validatie fouten" } diff --git a/web/public/locales/nl/components/dialog.json b/web/public/locales/nl/components/dialog.json index 4e7fd083b8..a69f382f37 100644 --- a/web/public/locales/nl/components/dialog.json +++ b/web/public/locales/nl/components/dialog.json @@ -67,11 +67,14 @@ "endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn" }, "success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.", - "view": "Weergeven" + "view": "Weergeven", + "queued": "Export staat in de wachtrij. Bekijk voortgang op de exports pagina." }, "fromTimeline": { "saveExport": "Export opslaan", - "previewExport": "Export vooraf bekijken" + "previewExport": "Export vooraf bekijken", + "useThisRange": "Gebruik dit bereik", + "queueingExport": "Export in wachtrij zetten..." }, "export": "Exporteren", "selectOrExport": "Selecteren of exporteren", @@ -107,7 +110,17 @@ "title_one": "Review 1 exporteren", "title_other": "{{count}} reviews exporteren", "description": "Exporteer alle geselecteerde reviews. Alle exports worden samengevoegd in één case.", - "descriptionNoCase": "Exporteer elke geselecteerde review." + "descriptionNoCase": "Exporteer elke geselecteerde review.", + "toast": { + "startedNoCase_one": "1 export gestart.", + "startedNoCase_other": "{{count}} Exports gestart.", + "partial": "{{successful}} van {{total}} exports gestart. Mislukte items: {{failedItems}}", + "failed": "Het starten van {{total}} exports is mislukt. Mislukt: {{failedItems}}" + }, + "exportingButton": "Exporteren...", + "exportButton_one": "Exporteer 1 revisie", + "exportButton_other": "Exporteer {{count}} revisies", + "caseNamePlaceholder": "Inspecteer export - {{date}}" } }, "streaming": { @@ -155,6 +168,14 @@ "error": "Verwijderen mislukt: {{error}}", "success": "De videobeelden die aan de geselecteerde beoordelingsitems zijn gekoppeld, zijn succesvol verwijderd." } + }, + "shareTimestamp": { + "label": "Tijdstempel delen", + "title": "Tijdstempel delen", + "description": "Deel een URL met tijdstempel van de huidige positie in de afspeler of kies een aangepaste tijdstempel. Let op: dit is geen openbare deel-URL en is alleen toegankelijk voor gebruikers die toegang hebben tot Frigate en deze camera.", + "custom": "Aangepaste tijdstempel", + "button": "Deel tijdstempel URL", + "shareTitle": "Frigate Beoordelingstijdstempel: {{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/pl/common.json b/web/public/locales/pl/common.json index 3d0c5bcc7f..f9238b4b72 100644 --- a/web/public/locales/pl/common.json +++ b/web/public/locales/pl/common.json @@ -167,7 +167,8 @@ "saveAll": "Zapisz wszystkie", "savingAll": "Zapisywanie wszystkich…", "undoAll": "Cofnij wszystko", - "retry": "Powtórz" + "retry": "Powtórz", + "resetToGlobal": "Przywróć ustawienie globalne" }, "menu": { "system": "System", @@ -274,7 +275,10 @@ "settings": "Ustawienia", "export": "Eksportuj", "classification": "Klasyfikacja", - "profiles": "Profile" + "profiles": "Profile", + "actions": "Akcje", + "features": "Funkcje", + "chat": "Czat" }, "role": { "viewer": "Przeglądający", @@ -328,5 +332,10 @@ "field": { "optional": "Opcjonalny", "internalID": "Wewnętrzny identyfikator używany przez Frigate w konfiguracji i bazie danych" + }, + "no_items": "Brak elementów", + "validation_errors": "Błędy walidacji", + "credentialField": { + "savedPlaceholder": "Zapisano — pozostaw puste, aby zachować bieżącą wartość" } } diff --git a/web/public/locales/pl/components/dialog.json b/web/public/locales/pl/components/dialog.json index 06408f81bf..c2cde61599 100644 --- a/web/public/locales/pl/components/dialog.json +++ b/web/public/locales/pl/components/dialog.json @@ -72,11 +72,24 @@ "endTimeMustAfterStartTime": "Czas zakończenia musi być późniejszy niż czas rozpoczęcia", "noVaildTimeSelected": "Nie wybrano prawidłowego zakresu czasu" }, - "view": "Widok" + "view": "Widok", + "queued": "Eksport dodany do kolejki. Postęp można sprawdzić na stronie eksportów.", + "batchSuccess_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.", + "batchSuccess_few": "Rozpoczęto {{count}} eksporty Otwieranie przypadku.", + "batchSuccess_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "batchPartial": "Rozpoczęto {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchFailed": "Nie udało się uruchomić {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchQueuedSuccess_one": "Eksport dodany do kolejki. Otwieranie przypadku.", + "batchQueuedSuccess_few": "Dodano {{count}} eksportydo kolejki. Otwieranie przypadku.", + "batchQueuedSuccess_many": "Dodano {{count}} eksportów do kolejki. Otwieranie przypadku.", + "batchQueuedPartial": "Dodano do kolejki {{successful}} z {{total}} eksportów. Nieudane kamery: {{failedCameras}}", + "batchQueueFailed": "Nie udało się dodać do kolejki {{total}} eksportów. Nieudane kamery: {{failedCameras}}" }, "fromTimeline": { "saveExport": "Zapisz Eksport", - "previewExport": "Podgląd Eksportu" + "previewExport": "Podgląd Eksportu", + "queueingExport": "Kolejkowanie eksportu...", + "useThisRange": "Użyj tego zakresu" }, "multiCamera": { "timeRange": "Zakres czasu", @@ -88,7 +101,54 @@ "detectionCount_one": "{{count}} śledzony obiekt", "detectionCount_few": "{{count}} śledzone obiekty", "detectionCount_many": "{{count}} śledzonych obiektów", - "nameLabel": "Wyeksportuj nazwę" + "nameLabel": "Wyeksportuj nazwę", + "namePlaceholder": "Opcjonalna nazwa bazowa eksportów", + "queueingButton": "Kolejkowanie eksportów...", + "exportButton_one": "Eksport 1 kamery", + "exportButton_few": "Eksport {{count}} kamer", + "exportButton_many": "Eksport {{count}} kamer", + "searchOrSelectGroup": "Szukaj lub wybierz grupę kamer ...", + "selectAll": "Wybierz wszystkie kamery", + "clearSelection": "Wyczyść wybór", + "selectWithActivity": "Kamery z obiektami śledzonymi", + "selectGroup": "Wybierz grupę", + "noMatchingCameras": "Nie ma kamer spełniających Twoje wyszukiwanie", + "selectedCount": "{{selected}} / {{total}} wybrane" + }, + "case": { + "newCaseOption": "Utwórz nowy przypadek", + "newCaseNamePlaceholder": "Nazwa nowego przypadku", + "newCaseDescriptionPlaceholder": "Opis przypadku", + "label": "Przypadek", + "nonAdminHelp": "Dla tych eksportów zostanie utworzony nowy przypadek.", + "placeholder": "Wybierz przypadek" + }, + "queueing": "Dodawanie eksportu do kolejki...", + "tabs": { + "export": "Pojedyncza kamera", + "multiCamera": "Wiele kamer" + }, + "multi": { + "title_one": "Eksport 1 recenzji", + "title_few": "Eksport {{count}} recenzji", + "title_many": "Eksport {{count}} recenzji", + "description": "Eksportuj każdą wybraną recenzję. Wszystkie eksporty zostaną pogrupowane w jednym przypadku.", + "descriptionNoCase": "Eksportuj każdą wybraną recenzję.", + "caseNamePlaceholder": "Eksport recenzji – {{date}}", + "exportButton_one": "Eksport 1 recenzji", + "exportButton_few": "Eksport {{count}} recenzji", + "exportButton_many": "Eksport {{count}} recenzji", + "exportingButton": "Eksportowanie...", + "toast": { + "started_one": "Rozpoczęto 1 eksport. Otwieranie przypadku.", + "started_few": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "started_many": "Rozpoczęto {{count}} eksportów. Otwieranie przypadku.", + "startedNoCase_one": "Rozpoczęto 1 eksport.", + "startedNoCase_few": "Rozpoczęto {{count}} eksportów.", + "startedNoCase_many": "Rozpoczęto {{count}} eksportów.", + "partial": "Rozpoczęto {{successful}} z {{total}} eksportów. Niepowodzenia: {{failedItems}}", + "failed": "Nie udało się uruchomić {{total}} eksportów. Niepowodzenia: {{failedItems}}" + } } }, "recording": { @@ -107,6 +167,14 @@ "success": "Nagrania wideo powiązane z wybranymi elementami przeglądu zostały pomyślnie usunięte.", "error": "Nie udało się usunąć: {{error}}" } + }, + "shareTimestamp": { + "label": "Udostępnij znacznik czasu", + "title": "Udostępnij znacznik czasu", + "description": "Udostępnij adres URL z aktualnym znacznikiem czasu pozycji odtwarzacza lub wybierz niestandardowy znacznik czasu. Należy pamiętać, że nie jest to publiczny link udostępniania i jest dostępny wyłącznie dla użytkowników mających dostęp do Frigate i tej kamery.", + "custom": "Własny znacznik czasu", + "button": "Udostępnij adres URL ze znacznikiem czasu", + "shareTitle": "Znacznik czasu przeglądu Frigate: {{camera}}" } }, "streaming": { diff --git a/web/public/locales/pl/config/cameras.json b/web/public/locales/pl/config/cameras.json index b7430d75d6..f890558aa7 100644 --- a/web/public/locales/pl/config/cameras.json +++ b/web/public/locales/pl/config/cameras.json @@ -176,7 +176,7 @@ "description": "Włączone" }, "audio": { - "label": "Zdarzenia audio", + "label": "Wykrywanie dźwięku", "description": "Ustawienia detekcji zdarzeń audio dla tej kamery.", "enabled": { "label": "Włącz detekcję audio", @@ -196,7 +196,11 @@ }, "filters": { "label": "Filtry audio", - "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów." + "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.", + "threshold": { + "label": "Minimalna pewność wykrycia dźwięku", + "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone." + } }, "enabled_in_config": { "label": "Pierwotny stan audio", @@ -224,10 +228,18 @@ }, "birdseye": { "mode": { - "label": "Tryb śledzenia" + "label": "Tryb śledzenia", + "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”." }, "order": { - "label": "Pozycja" + "label": "Pozycja", + "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka." + }, + "label": "Widok z lotu ptaka", + "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.", + "enabled": { + "label": "Włącz widok z lotu ptaka", + "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka." } }, "detect": { @@ -236,7 +248,58 @@ "description": "Włącza lub wyłącza wykrywanie obiektów dla tej kamery." }, "stationary": { - "label": "Konfiguracja obiektów stacjonarnych" + "label": "Konfiguracja obiektów stacjonarnych", + "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.", + "interval": { + "label": "Okres bezruchu", + "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych." + }, + "threshold": { + "label": "Próg bezruchu", + "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy." + }, + "max_frames": { + "label": "Maksymalna liczba klatek", + "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.", + "default": { + "label": "Domyślna maks. liczba klatek", + "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem." + }, + "objects": { + "label": "Maksymalna liczba klatek obiektu", + "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów." + } + }, + "classifier": { + "label": "Włącz klasyfikator wizualny", + "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą." + } + }, + "label": "Wykrywanie obiektów", + "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.", + "height": { + "label": "Wysokość wykrywania", + "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "width": { + "label": "Szerokość wykrywania", + "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "fps": { + "label": "FPS wykrywania", + "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)." + }, + "min_initialized": { + "label": "Minimalna liczba klatek inicjalizacyjnych", + "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2." + }, + "max_disappeared": { + "label": "Maksymalna liczba pominiętych klatek", + "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony." + }, + "annotation_offset": { + "label": "Przesunięcie oznaczenia", + "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna." } }, "face_recognition": { @@ -245,6 +308,33 @@ "enabled": { "label": "Włącz rozpoznawanie twarzy", "description": "Włącz lub wyłącz rozpoznawanie twarzy." + }, + "min_area": { + "label": "Minimalny obszar twarzy", + "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania." + } + }, + "ffmpeg": { + "label": "Strumienie (FFmpeg)", + "output_args": { + "detect": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji." + }, + "record": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu." + } + }, + "retry_interval": { + "label": "Czas ponownej próby FFmpeg", + "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10." + }, + "apple_compatibility": { + "label": "Kompatybilność z Apple", + "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna." } } } diff --git a/web/public/locales/pl/config/global.json b/web/public/locales/pl/config/global.json index 8d16de3feb..59d2232a06 100644 --- a/web/public/locales/pl/config/global.json +++ b/web/public/locales/pl/config/global.json @@ -1,6 +1,6 @@ { "audio": { - "label": "Zdarzenia audio", + "label": "Wykrywanie dźwięku", "enabled": { "label": "Włącz detekcję audio" }, @@ -18,7 +18,11 @@ }, "filters": { "label": "Filtry audio", - "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów." + "description": "Ustawienia filtrów dla poszczególnych typów audio, takie jak progi pewności, używane do redukcji fałszywych alarmów.", + "threshold": { + "label": "Minimalna pewność wykrycia dźwięku", + "description": "Minimalny próg pewności, aby zdarzenie audio zostało zliczone." + } }, "enabled_in_config": { "label": "Pierwotny stan audio", @@ -71,10 +75,18 @@ }, "birdseye": { "mode": { - "label": "Tryb śledzenia" + "label": "Tryb śledzenia", + "description": "Tryb uwzględniania kamer w widoku z lotu ptaka: „obiekty”, „ruch” lub „ciągły”." }, "order": { - "label": "Pozycja" + "label": "Pozycja", + "description": "Pozycja numeryczna określająca kolejność kamery w układzie widoku z lotu ptaka." + }, + "label": "Widok z lotu ptaka", + "description": "Ustawienia widoku z lotu ptaka „Birdseye”, który łączy obraz z wielu kamer w jeden układ.", + "enabled": { + "label": "Włącz widok z lotu ptaka", + "description": "Włącz lub wyłącz funkcję widoku z lotu ptaka." } }, "detect": { @@ -82,13 +94,90 @@ "label": "Włącz wykrywanie obiektów" }, "stationary": { - "label": "Konfiguracja obiektów stacjonarnych" + "label": "Konfiguracja obiektów stacjonarnych", + "description": "Ustawienia wykrywania i obsługi obiektów statycznych przez określony czas.", + "interval": { + "label": "Okres bezruchu", + "description": "Częstotliwość (w klatkach) sprawdzania detekcji dla obiektów nieruchomych." + }, + "threshold": { + "label": "Próg bezruchu", + "description": "Liczba klatek bez zmiany pozycji potrzebna do uznania obiektu za nieruchomy." + }, + "max_frames": { + "label": "Maksymalna liczba klatek", + "description": "Ogranicza czas śledzenia nieruchomych obiektów przed ich usunięciem.", + "default": { + "label": "Domyślna maks. liczba klatek", + "description": "Domyślna maksymalna liczba klatek śledzenia nieruchomego obiektu przed zatrzymaniem." + }, + "objects": { + "label": "Maksymalna liczba klatek obiektu", + "description": "Nadpisania dla poszczególnych obiektów określające maksymalną liczbę klatek śledzenia nieruchomych obiektów." + } + }, + "classifier": { + "label": "Włącz klasyfikator wizualny", + "description": "Użyj klasyfikatora wizualnego do wykrywania rzeczywiście nieruchomych obiektów, nawet gdy ramki ograniczające drżą." + } + }, + "label": "Wykrywanie obiektów", + "description": "Ustawienia roli „detection/detect” używanej do wykrywania obiektów i inicjalizacji trackerów.", + "height": { + "label": "Wysokość wykrywania", + "description": "Wysokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "width": { + "label": "Szerokość wykrywania", + "description": "Szerokość (w pikselach) klatek używanych w strumieniu detekcji; pozostaw puste, aby użyć natywnej rozdzielczości strumienia." + }, + "fps": { + "label": "FPS wykrywania", + "description": "Docelowa liczba klatek na sekundę używana do detekcji; niższe wartości zmniejszają użycie CPU (zalecana wartość to 5, ustaw wyższą — maksymalnie 10 — tylko jeśli śledzisz bardzo szybko poruszające się obiekty)." + }, + "min_initialized": { + "label": "Minimalna liczba klatek inicjalizacyjnych", + "description": "Liczba kolejnych trafień detekcji wymagana do utworzenia śledzonego obiektu. Zwiększenie wartości zmniejsza liczbę fałszywych inicjalizacji. Wartość domyślna to fps podzielone przez 2." + }, + "max_disappeared": { + "label": "Maksymalna liczba pominiętych klatek", + "description": "Liczba klatek bez detekcji, po której obiekt śledzony jest uznawany za utracony." + }, + "annotation_offset": { + "label": "Przesunięcie oznaczenia", + "description": "Liczba milisekund przesunięcia adnotacji detekcji w celu lepszego dopasowania ramek ograniczających na osi czasu do nagrań; wartość może być dodatnia lub ujemna." } }, "face_recognition": { "label": "Rozpoznawanie twarzy", "enabled": { "label": "Włącz rozpoznawanie twarzy" + }, + "min_area": { + "label": "Minimalny obszar twarzy", + "description": "Minimalny obszar twarzy (w pikselach) wymagany do podjęcia próby rozpoznania." + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą detekcji." + }, + "record": { + "description": "Domyślne argumenty wyjściowe dla strumieni z rolą zapisu." + } + }, + "retry_interval": { + "label": "Czas ponownej próby FFmpeg", + "description": "Czas (w sekundach) przed ponownym uruchomieniem utraconego strumienia kamery. Domyślnie: 10." + }, + "apple_compatibility": { + "label": "Kompatybilność z Apple", + "description": "Włącz tagowanie HEVC, aby zapewnić lepszą zgodność z odtwarzaczami Apple podczas nagrywania w formacie H.265." + }, + "gpu": { + "label": "Indeks GPU", + "description": "Domyślny indeks karty graficznej używany do akceleracji sprzętowej, jeśli jest dostępna." } } } diff --git a/web/public/locales/pl/views/classificationModel.json b/web/public/locales/pl/views/classificationModel.json index a56cd62b3c..b5a048882e 100644 --- a/web/public/locales/pl/views/classificationModel.json +++ b/web/public/locales/pl/views/classificationModel.json @@ -30,7 +30,8 @@ "trainedModel": "Model pomyślnie wytrenowany.", "trainingModel": "Pomyślnie uruchomiono trenowanie modelu.", "updatedModel": "Pomyślnie zaktualizowane ustawienia modelu", - "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}" + "renamedCategory": "Pomyślnie zmieniono nazwę klasy na {{name}}", + "reclassifiedImage": "Pomyślne przeklasyfikowanie obrazu" }, "error": { "deleteImageFailed": "Nie udało się usunąć: {{errorMessage}}", @@ -40,7 +41,8 @@ "trainingFailed": "Trening modelu zakończył się niepowodzeniem. Sprawdź logi Frigate aby uzyskać więcej informacji.", "updateModelFailed": "Nie udało się zaktualizować modelu: {{errorMessage}}", "trainingFailedToStart": "Nie udało się rozpocząć trenowania modelu: {{errorMessage}}", - "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}" + "renameCategoryFailed": "Nie udało się zmienić nazwy klasy: {{errorMessage}}", + "reclassifyFailed": "Niepowodzenie zmiany klasyfikacji obrazu: {{errorMessage}}" } }, "deleteCategory": { @@ -191,7 +193,14 @@ "missingStatesWarning": { "title": "Przykłady brakujących stanów", "description": "Aby uzyskać najlepsze wyniki, zaleca się wybranie przykładów dla wszystkich stanów. Można kontynuować bez wybierania wszystkich stanów, ale model nie zostanie wytrenowany, dopóki wszystkie stany nie będą miały obrazów. Po kontynuowaniu należy użyć widoku Ostatnie klasyfikacje, aby sklasyfikować obrazy dla brakujących stanów, a następnie wytrenować model." + }, + "refreshExamples": "Generuj nowe przykłady", + "refreshConfirm": { + "title": "Czy generować nowe przykłady?", + "description": "Spowoduje to wygenerowanie nowego zestawu obrazów i wyczyszczenie wszystkich zaznaczeń, w tym dotychczasowych klas. Konieczne będzie ponowne wybranie przykładów dla wszystkich klas." } } - } + }, + "reclassifyImageAs": "Klasyfikuj jako:", + "reclassifyImage": "Zmiana klasyfikacji obrazu" } diff --git a/web/public/locales/pt-BR/audio.json b/web/public/locales/pt-BR/audio.json index 552a135650..1444d4d6a4 100644 --- a/web/public/locales/pt-BR/audio.json +++ b/web/public/locales/pt-BR/audio.json @@ -497,5 +497,6 @@ "echo": "Eco", "mains_hum": "Zumbido Elétrico", "sidetone": "Retorno de Voz", - "throbbing": "Latejante" + "throbbing": "Latejante", + "sodeling": "Soldagem" } diff --git a/web/public/locales/pt-BR/common.json b/web/public/locales/pt-BR/common.json index b2ebe49c28..3ce687cc73 100644 --- a/web/public/locales/pt-BR/common.json +++ b/web/public/locales/pt-BR/common.json @@ -210,7 +210,8 @@ "id": "Bahasa Indonesia (Indonésio)", "ur": "اردو (Urdu)", "hr": "Hrvatski (Croata)", - "bs": "Bosanski (Bósnio)" + "bs": "Bosanski (Bósnio)", + "zhHant": "Chinês Tradicional" }, "systemLogs": "Logs de sistema", "settings": "Configurações", diff --git a/web/public/locales/pt-BR/components/camera.json b/web/public/locales/pt-BR/components/camera.json index f82636861d..a746960980 100644 --- a/web/public/locales/pt-BR/components/camera.json +++ b/web/public/locales/pt-BR/components/camera.json @@ -68,7 +68,10 @@ } }, "birdseye": "Visão Panorâmica" - } + }, + "showAll": "Exibir todos os grupos de câmeras", + "showLess": "Mostrar menos", + "editGroups": "Editar grupos de câmeras" }, "debug": { "options": { diff --git a/web/public/locales/pt-BR/components/dialog.json b/web/public/locales/pt-BR/components/dialog.json index 788fe118d7..7f877adadb 100644 --- a/web/public/locales/pt-BR/components/dialog.json +++ b/web/public/locales/pt-BR/components/dialog.json @@ -24,6 +24,9 @@ }, "state": { "submitted": "Enviado" + }, + "toast": { + "error": "Falha ao enviar para o Frigate+. Verifique sua conexão de rede e tente novamente." } } }, @@ -56,15 +59,25 @@ "toast": { "success": "Exportação iniciada com sucesso. Veja o arquivo na tela exportar.", "error": { - "failed": "Falha em iniciar exportação: {{error}}", + "failed": "Falha ao iniciar exportação: {{error}}", "endTimeMustAfterStartTime": "Tempo de finalização deve ser após tempo de início", "noVaildTimeSelected": "Nenhuma faixa de tempo válida selecionada" }, - "view": "Ver" + "view": "Ver", + "batchQueuedSuccess_one": "1 exportação na fila. Abrindo o caso agora.", + "batchQueuedSuccess_many": "{{count}} exportações colocadas na fila. Abrindo o caso agora.", + "batchQueuedSuccess_other": "", + "batchQueuedPartial": "{{successful}} de {{total}} exportações na fila. Câmeras com falha: {{failedCameras}}", + "batchQueueFailed": "Falha ao enfileirar {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "batchPartial": "Iniciadas {{successful}} de {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "batchFailed": "Falha ao iniciar {{total}} exportações. Câmeras com falha: {{failedCameras}}", + "queued": "Exportação na fila. Acompanhe o progresso na página de exportações." }, "fromTimeline": { "saveExport": "Salvar Exportação", - "previewExport": "Pré-Visualizar Exportação" + "previewExport": "Pré-Visualizar Exportação", + "queueingExport": "Enfileirando exportação...", + "useThisRange": "Use esta faixa" }, "case": { "label": "Caso", @@ -94,7 +107,14 @@ "queueingButton": "Exportações na fila...", "exportButton_one": "Exportar câmera", "exportButton_many": "Exportar {{count}} câmeras", - "exportButton_other": "Exportar {{count}} câmeras" + "exportButton_other": "Exportar {{count}} câmeras", + "searchOrSelectGroup": "Procurar, ou selecionar um grupo de câmeras...", + "selectAll": "Selecionar todas as câmeras", + "clearSelection": "Limpar seleção", + "selectWithActivity": "Câmeras com rastreamento de objetos", + "selectGroup": "Selecione grupo", + "noMatchingCameras": "Nenhuma câmera corresponde à sua pesquisa", + "selectedCount": "{{selected}} / {{total}} selecionados" }, "multi": { "title_one": "Exportar análise", @@ -106,7 +126,14 @@ "exportButton_one": "Exportar análise", "exportButton_many": "Exportar {{count}} análises", "exportButton_other": "Exportar {{count}} análises", - "exportingButton": "Exportando..." + "exportingButton": "Exportando...", + "toast": { + "started_one": "Uma exportação iniciada. Abrindo o caso agora.", + "started_many": "Iniciadas {{count}} exportações. Abrindo o caso agora.", + "started_other": "", + "failed": "Falha ao iniciar {{total}} exportações. Falhas: {{failedItems}}", + "partial": "Iniciadas {{successful}} de {{total}} exportações. Falhas: {{failedItems}}" + } } }, "streaming": { @@ -154,6 +181,14 @@ "export": "Exportar", "deleteNow": "Deletar Agora", "markAsUnreviewed": "Marcar como não revisado" + }, + "shareTimestamp": { + "label": "Compartilhar marca de tempo", + "title": "Compartilhar marca de tempo", + "description": "Compartilhe uma URL com marca de tempo da posição atual da reprodução ou escolha uma marca de tempo personalizada. Observe que esta não é uma URL de compartilhamento público e só pode ser acessada por usuários que tenham acesso ao Frigate e a esta câmera.", + "custom": "Marcação de data/hora personalizado", + "button": "Compartilhar URL com marcação de tempo", + "shareTitle": "Marcação de data/hora da análise do Frigate: {{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/pt-BR/components/player.json b/web/public/locales/pt-BR/components/player.json index 370565b633..e783725e8b 100644 --- a/web/public/locales/pt-BR/components/player.json +++ b/web/public/locales/pt-BR/components/player.json @@ -4,7 +4,8 @@ "noPreviewFoundFor": "Nenhuma Pré-Visualização Encontrada em {{cameraName}}", "submitFrigatePlus": { "title": "Enviar esse frame para Frigate+?", - "submit": "Enviar" + "submit": "Enviar", + "previewError": "Não foi possível carregar a prévia do instantâneo. A gravação pode não estar disponível no momento." }, "livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para esse tipo de transmissão ao vivo.", "streamOffline": { @@ -47,5 +48,6 @@ "error": { "submitFrigatePlusFailed": "Falha em submeter quadro ao Frigate+" } - } + }, + "cameraOff": "A câmera está desligada" } diff --git a/web/public/locales/pt-BR/config/cameras.json b/web/public/locales/pt-BR/config/cameras.json index 48b0de7d78..4be948c526 100644 --- a/web/public/locales/pt-BR/config/cameras.json +++ b/web/public/locales/pt-BR/config/cameras.json @@ -12,7 +12,7 @@ "description": "Habilitado" }, "audio": { - "label": "Eventos de áudio", + "label": "Detecção de áudio", "description": "Configurações para detecção de eventos baseados em áudio para esta câmera.", "enabled": { "label": "Habilitar detecção de áudio", @@ -28,11 +28,15 @@ }, "listen": { "label": "Tipos de escuta", - "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)." + "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)." }, "filters": { "label": "Filtros de áudio", - "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos." + "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.", + "threshold": { + "label": "Confiança mínima do áudio", + "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado." + } }, "enabled_in_config": { "label": "Estado de áudio original", @@ -47,11 +51,495 @@ "audio_transcription": { "label": "Transcrição de áudio", "enabled": { - "label": "Habilitar transcrição" + "label": "Habilitar transcrição", + "description": "Ative ou desative a transcrição de eventos de áudio acionada manualmente." }, "live_enabled": { - "label": "Transcrição em tempo real" + "label": "Transcrição em tempo real", + "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido." }, - "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real." + "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real.", + "enabled_in_config": { + "label": "Estado original da transcrição" + } + }, + "detect": { + "enabled": { + "label": "Ativar detecção de objetos", + "description": "Ative ou desative a detecção de objetos para esta câmera." + }, + "height": { + "label": "Detectar altura", + "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "width": { + "label": "Detectar largura", + "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "fps": { + "label": "Detectar FPS", + "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)." + }, + "min_initialized": { + "label": "Quadros de inicialização mínimos", + "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2." + }, + "max_disappeared": { + "label": "Máximo de quadros perdidos", + "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido." + }, + "stationary": { + "label": "Configuração de objetos estáticos", + "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.", + "interval": { + "label": "Intervalo estacionário", + "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário." + }, + "threshold": { + "label": "Limiar estacionário", + "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário." + }, + "max_frames": { + "label": "Quadros máximos", + "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.", + "default": { + "label": "Máximo de quadros padrão", + "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar." + }, + "objects": { + "label": "Quadros máximos do objeto", + "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários." + } + }, + "classifier": { + "label": "Ativar classificador visual", + "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam." + } + }, + "annotation_offset": { + "label": "Deslocamento da anotação", + "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo." + }, + "label": "Detecção de Objetos", + "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores." + }, + "face_recognition": { + "label": "Reconhecimento facial", + "description": "Configurações de detecção e reconhecimento facial para esta câmera.", + "enabled": { + "label": "Ativar reconhecimento facial", + "description": "Ative ou desative o reconhecimento facial." + }, + "min_area": { + "label": "Área mínima do rosto", + "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento." + } + }, + "ffmpeg": { + "label": "Fluxos (FFmpeg)", + "description": "Entradas de fluxo de câmera e opções do FFmpeg, incluindo caminho do binário, argumentos, aceleração de hardware (hwaccel) e argumentos de saída por função.", + "path": { + "label": "Caminho do FFmpeg", + "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais passados para processos do FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg." + }, + "output_args": { + "label": "Argumentos de saída", + "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.", + "detect": { + "label": "Detectar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de detecção de função." + }, + "record": { + "label": "Registrar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de função de registro." + } + }, + "retry_interval": { + "label": "Tempo de nova tentativa do FFmpeg", + "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10." + }, + "apple_compatibility": { + "label": "Compatibilidade com a Apple", + "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265." + }, + "gpu": { + "label": "Índice da GPU", + "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível." + }, + "inputs": { + "label": "Entradas de câmera", + "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.", + "path": { + "label": "Caminho de entrada", + "description": "URL ou caminho do fluxo de entrada da câmera." + }, + "roles": { + "label": "Funções de entrada", + "description": "Funções para este fluxo de entrada." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais do FFmpeg para este fluxo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para este fluxo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este fluxo." + } + } + }, + "birdseye": { + "mode": { + "label": "Modo de rastreamento", + "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'." + }, + "order": { + "label": "Posição", + "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)." + }, + "label": "Birdseye", + "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.", + "enabled": { + "label": "Ativar Birdseye" + } + }, + "live": { + "label": "Reprodução ao vivo", + "description": "Configurações utilizadas pela interface web para controlar a seleção, a resolução e a qualidade da transmissão ao vivo.", + "streams": { + "label": "Nomes de transmissões ao vivo", + "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo." + }, + "height": { + "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção." + }, + "quality": { + "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)." + } + }, + "motion": { + "enabled": { + "label": "Ativar detecção de movimento", + "description": "Ative ou desative a detecção de movimento para esta câmera." + }, + "threshold": { + "label": "Limiar de movimento", + "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)." + }, + "lightning_threshold": { + "label": "Limiar de relâmpago", + "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos." + }, + "skip_motion_threshold": { + "label": "Limite de movimento para pular", + "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso." + }, + "improve_contrast": { + "label": "Melhorar contraste", + "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado." + }, + "delta_alpha": { + "label": "Delta Alfa", + "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento." + }, + "frame_alpha": { + "label": "Quadro alfa", + "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento." + }, + "frame_height": { + "label": "Altura da quadro", + "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento." + }, + "mask": { + "label": "Coordenadas da máscara", + "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas." + }, + "mqtt_off_delay": { + "label": "Atraso de desligamento MQTT", + "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT." + }, + "enabled_in_config": { + "label": "Estado de movimento original", + "description": "Indica se a detecção de movimento estava habilitada na configuração estática original." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "label": "Detecção de movimento", + "description": "Configurações padrão de detecção de movimento para esta câmera." + }, + "objects": { + "label": "Objetos", + "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.", + "track": { + "label": "Objetos a rastrear", + "description": "Lista de rótulos de objetos a serem rastreados para esta câmera." + }, + "filters": { + "label": "Filtros de objeto", + "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).", + "min_area": { + "label": "Área mínima do objeto", + "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)." + }, + "max_area": { + "label": "Área máxima do objeto", + "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)." + }, + "min_ratio": { + "label": "Razão de aspecto mínimo", + "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique." + }, + "max_ratio": { + "label": "Razão de aspecto máxima", + "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique." + }, + "threshold": { + "label": "Limiar de confiança", + "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo." + }, + "min_score": { + "label": "Confiança mínima", + "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado." + }, + "mask": { + "label": "Máscara com filtro", + "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro." + }, + "raw_mask": { + "label": "Máscara Bruta" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "genai": { + "label": "Configuração de objeto da GenAI", + "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.", + "enabled": { + "label": "Ativar GenAI", + "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI." + }, + "use_snapshot": { + "label": "Usar instantâneos", + "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI." + }, + "prompt": { + "label": "Sugestão de legenda", + "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI." + }, + "object_prompts": { + "label": "Prompts de objeto", + "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI." + }, + "debug_save_thumbnails": { + "label": "Salvar miniaturas", + "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão." + }, + "send_triggers": { + "label": "Gatilhos de GenAI", + "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).", + "tracked_object_end": { + "label": "Enviar no fim", + "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar." + }, + "after_significant_updates": { + "label": "Acionamento antecipado da GenAI", + "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado." + } + }, + "enabled_in_config": { + "label": "Estado original da GenAI", + "description": "Indica se a GenAI foi habilitada na configuração estática original." + } + } + }, + "lpr": { + "enabled": { + "label": "Ativar LPR", + "description": "Ative ou desative o LPR nesta câmera." + }, + "expire_time": { + "label": "Segundos para expirar", + "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)." + }, + "min_area": { + "label": "Área mínima da placa", + "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento." + }, + "enhancement": { + "label": "Nível de aprimoramento", + "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela." + } + }, + "record": { + "label": "Gravação", + "description": "Configurações de gravação e retenção para esta câmera.", + "enabled": { + "label": "Ativar gravação", + "description": "Ative ou desative a gravação para esta câmera." + }, + "expire_interval": { + "label": "Intervalo de limpeza de gravação", + "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados." + }, + "continuous": { + "label": "Retenção contínua", + "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "motion": { + "label": "Retenção de movimento", + "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "detections": { + "label": "Retenção de detecção", + "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)." + } + } + }, + "alerts": { + "label": "Retenção de alertas", + "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)." + } + } + }, + "export": { + "label": "Exportar configuração", + "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.", + "hwaccel_args": { + "label": "Exportar argumentos de hwaccel", + "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação." + }, + "max_concurrent": { + "label": "Máximo de exportações simultâneas", + "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente." + }, + "chapters": { + "label": "Metadados de capítulos para incorporar nas gravações exportadas" + } + }, + "preview": { + "label": "Pré-visualizar configuração", + "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.", + "quality": { + "label": "Qualidade da pré-visualização", + "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)." + } + }, + "enabled_in_config": { + "label": "Estado original da gravação", + "description": "Indica se a gravação foi habilitada na configuração estática original." + } + }, + "review": { + "label": "Rever", + "description": "Configurações que controlam alertas, detecções e resumos de revisão por GenAI utilizados pela interface do usuário e pelo armazenamento desta câmera.", + "alerts": { + "label": "Configuração de alertas", + "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.", + "enabled": { + "label": "Ativar alertas", + "description": "Ative ou desative a geração de alertas para esta câmera." + }, + "labels": { + "label": "Etiquetas de alerta", + "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona." + }, + "enabled_in_config": { + "label": "Estado dos alertas originais", + "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática." + }, + "cutoff_time": { + "label": "Horário limite dos alertas", + "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta." + } + }, + "detections": { + "label": "Configuração de detecções", + "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.", + "enabled": { + "label": "Ativar detecções", + "description": "Ative ou desative eventos de detecção para esta câmera." + } + } } } diff --git a/web/public/locales/pt-BR/config/global.json b/web/public/locales/pt-BR/config/global.json index a503c8ee94..1cf4a289ef 100644 --- a/web/public/locales/pt-BR/config/global.json +++ b/web/public/locales/pt-BR/config/global.json @@ -34,11 +34,15 @@ }, "listen": { "label": "Tipos de escuta", - "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)." + "description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)." }, "filters": { "label": "Filtros de áudio", - "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos." + "description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.", + "threshold": { + "label": "Confiança mínima do áudio", + "description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado." + } }, "enabled_in_config": { "label": "Estado de áudio original", @@ -47,7 +51,8 @@ "num_threads": { "label": "Threads de detecção", "description": "Número de threads a serem usadas para o processamento de detecção de áudio." - } + }, + "label": "Detecção de áudio" }, "auth": { "label": "Autenticação", @@ -82,8 +87,473 @@ "audio_transcription": { "label": "Transcrição de áudio", "live_enabled": { - "label": "Transcrição em tempo real" + "label": "Transcrição em tempo real", + "description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido." }, "description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real." + }, + "detect": { + "enabled": { + "label": "Ativar detecção de objetos" + }, + "height": { + "label": "Detectar altura", + "description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "width": { + "label": "Detectar largura", + "description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo." + }, + "fps": { + "label": "Detectar FPS", + "description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)." + }, + "min_initialized": { + "label": "Quadros de inicialização mínimos", + "description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2." + }, + "max_disappeared": { + "label": "Máximo de quadros perdidos", + "description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido." + }, + "stationary": { + "label": "Configuração de objetos estáticos", + "description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.", + "interval": { + "label": "Intervalo estacionário", + "description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário." + }, + "threshold": { + "label": "Limiar estacionário", + "description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário." + }, + "max_frames": { + "label": "Quadros máximos", + "description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.", + "default": { + "label": "Máximo de quadros padrão", + "description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar." + }, + "objects": { + "label": "Quadros máximos do objeto", + "description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários." + } + }, + "classifier": { + "label": "Ativar classificador visual", + "description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam." + } + }, + "annotation_offset": { + "label": "Deslocamento da anotação", + "description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo." + }, + "label": "Detecção de Objetos", + "description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores." + }, + "face_recognition": { + "label": "Reconhecimento facial", + "enabled": { + "label": "Ativar reconhecimento facial" + }, + "min_area": { + "label": "Área mínima do rosto", + "description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento." + } + }, + "ffmpeg": { + "path": { + "label": "Caminho do FFmpeg", + "description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais passados para processos do FFmpeg." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada aplicados aos fluxos de entrada do FFmpeg." + }, + "output_args": { + "label": "Argumentos de saída", + "description": "Argumentos de saída padrão utilizados para diferentes funções do FFmpeg, como detecção e gravação.", + "detect": { + "label": "Detectar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de detecção de função." + }, + "record": { + "label": "Registrar argumentos de saída", + "description": "Argumentos de saída padrão para fluxos de função de registro." + } + }, + "retry_interval": { + "label": "Tempo de nova tentativa do FFmpeg", + "description": "Segundos de espera antes de tentar reconectar um fluxo de câmera após uma falha. O padrão é 10." + }, + "apple_compatibility": { + "label": "Compatibilidade com a Apple", + "description": "Ative a marcação HEVC para melhor compatibilidade com reprodutores da Apple ao gravar em H.265." + }, + "gpu": { + "label": "Índice da GPU", + "description": "Índice da GPU padrão usado para aceleração de hardware, se disponível." + }, + "inputs": { + "label": "Entradas de câmera", + "description": "Lista de definições de fluxo de entrada (caminhos e funções) para esta câmera.", + "path": { + "label": "Caminho de entrada", + "description": "URL ou caminho do fluxo de entrada da câmera." + }, + "roles": { + "label": "Funções de entrada", + "description": "Funções para este fluxo de entrada." + }, + "global_args": { + "label": "Argumentos globais do FFmpeg", + "description": "Argumentos globais do FFmpeg para este fluxo de entrada." + }, + "hwaccel_args": { + "label": "Argumentos de aceleração de hardware", + "description": "Argumentos de aceleração de hardware para este fluxo de entrada." + }, + "input_args": { + "label": "Argumentos de entrada", + "description": "Argumentos de entrada específicos para este fluxo." + } + } + }, + "birdseye": { + "mode": { + "label": "Modo de rastreamento", + "description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'." + }, + "order": { + "label": "Posição", + "description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)." + }, + "label": "Birdseye", + "description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.", + "enabled": { + "label": "Ativar Birdseye" + } + }, + "live": { + "label": "Reprodução ao vivo", + "streams": { + "label": "Nomes de transmissões ao vivo", + "description": "Mapeamento de nomes de fluxos configurados para nomes de restream/go2rtc usados na reprodução ao vivo." + }, + "height": { + "description": "Altura (em pixels) para renderizar a transmissão ao vivo jsmpeg na interface web; deve ser menor ou igual à altura da transmissão de detecção." + }, + "quality": { + "description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa)." + } + }, + "motion": { + "enabled": { + "label": "Ativar detecção de movimento" + }, + "threshold": { + "label": "Limiar de movimento", + "description": "Limiar de diferença de pixels utilizado pelo detector de movimento; valores mais altos reduzem a sensibilidade (faixa de 1 a 255)." + }, + "lightning_threshold": { + "label": "Limiar de relâmpago", + "description": "Limite para detectar e ignorar picos breves de luminosidade (valores mais baixos indicam maior sensibilidade; valores entre 0,3 e 1,0). Isso não impede totalmente a detecção de movimento; apenas faz com que o detector pare de analisar quadros adicionais assim que o limite é excedido. Gravações baseadas em movimento continuam sendo geradas durante esses eventos." + }, + "skip_motion_threshold": { + "label": "Limite de movimento para pular", + "description": "Se definido para um valor entre 0,0 e 1,0, e se uma proporção da imagem superior a esse valor sofrer alterações em um único quadro, o detector não retornará caixas de detecção de movimento e será recalibrado imediatamente. Isso pode economizar recursos da CPU e reduzir falsos positivos causados por relâmpagos, tempestades, etc., mas pode deixar passar eventos reais, como o rastreamento automático de um objeto por uma câmera PTZ. A escolha envolve um equilíbrio entre descartar alguns megabytes de gravações e analisar alguns clipes curtos. Deixe a opção não definida (None) para desativar esse recurso." + }, + "improve_contrast": { + "label": "Melhorar contraste", + "description": "Aplique melhoria de contraste aos quadros antes da análise de movimento para auxiliar na detecção." + }, + "contour_area": { + "label": "Área de contorno", + "description": "Área mínima do contorno, em pixels, necessária para que um contorno de movimento seja contabilizado." + }, + "delta_alpha": { + "label": "Delta Alfa", + "description": "Fator de *alpha blending* utilizado na diferenciação de quadros para o cálculo de movimento." + }, + "frame_alpha": { + "label": "Quadro alfa", + "description": "Valor alfa utilizado ao combinar quadros para o pré-processamento de movimento." + }, + "frame_height": { + "label": "Altura da quadro", + "description": "Altura em pixels para a qual os quadros são redimensionados ao calcular o movimento." + }, + "mask": { + "label": "Coordenadas da máscara", + "description": "Coordenadas x,y ordenadas que definem o polígono da máscara de movimento utilizado para incluir ou excluir áreas." + }, + "mqtt_off_delay": { + "label": "Atraso de desligamento MQTT", + "description": "Segundos de espera após o último movimento antes de publicar um estado \"off\" via MQTT." + }, + "enabled_in_config": { + "label": "Estado de movimento original", + "description": "Indica se a detecção de movimento estava habilitada na configuração estática original." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "label": "Detecção de movimento" + }, + "objects": { + "label": "Objetos", + "description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.", + "track": { + "label": "Objetos a rastrear" + }, + "filters": { + "label": "Filtros de objeto", + "description": "Filtros aplicados aos objetos detectados para reduzir falsos positivos (área, proporção, confiança).", + "min_area": { + "label": "Área mínima do objeto", + "description": "Área mínima da caixa delimitadora (em pixels ou porcentagem) necessária para este tipo de objeto. Pode ser especificada em pixels (inteiro) ou porcentagem (valor de ponto flutuante entre 0,000001 e 0,99)." + }, + "max_area": { + "label": "Área máxima do objeto", + "description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este tipo de objeto. Pode ser pixels (int) ou porcentagem (flutuar entre 0,000001 e 0,99)." + }, + "min_ratio": { + "label": "Razão de aspecto mínimo", + "description": "Razão mínima entre largura e altura necessária para que a caixa delimitadora se qualifique." + }, + "max_ratio": { + "label": "Razão de aspecto máxima", + "description": "Razão máxima entre largura e altura permitida para que a caixa delimitadora qualifique." + }, + "threshold": { + "label": "Limiar de confiança", + "description": "Limiar médio de confiança de detecção necessário para que o objeto seja considerado um verdadeiro positivo." + }, + "min_score": { + "label": "Confiança mínima", + "description": "Confiança mínima de detecção em um único quadro necessária para que o objeto seja contabilizado." + }, + "mask": { + "label": "Máscara com filtro", + "description": "Coordenadas do polígono que definem onde este filtro é aplicado dentro do quadro." + }, + "raw_mask": { + "label": "Máscara Bruta" + } + }, + "mask": { + "label": "Máscara de objeto", + "description": "Polígono de máscara usado para impedir a detecção de objetos em áreas específicas." + }, + "raw_mask": { + "label": "Máscara Bruta" + }, + "genai": { + "label": "Configuração de objeto da GenAI", + "description": "Opções de GenIA para descrever objetos rastreados e enviar quadros para geração.", + "enabled": { + "label": "Ativar GenAI", + "description": "Habilitar por padrão a geração de descrições para objetos rastreados usando GenAI." + }, + "use_snapshot": { + "label": "Usar instantâneos", + "description": "Use instantâneos de objetos em vez de miniaturas para a geração de descrições pela GenAI." + }, + "prompt": { + "label": "Sugestão de legenda", + "description": "Modelo de prompt padrão usado ao gerar descrições com GenAI." + }, + "object_prompts": { + "label": "Prompts de objeto", + "description": "Prompts por objeto para personalizar as saídas de GenAI para rótulos específicos." + }, + "objects": { + "label": "Objetos de GenAI", + "description": "Lista de rótulos de objetos a serem enviados à GenAI por padrão." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas que devem ser acessadas para que os objetos se qualifiquem para a geração de descrições pela GenAI." + }, + "debug_save_thumbnails": { + "label": "Salvar miniaturas", + "description": "Salve as miniaturas enviadas à GenAI para depuração e revisão." + }, + "send_triggers": { + "label": "Gatilhos de GenAI", + "description": "Define quando os quadros devem ser enviados para a GenAI (ao final, após atualizações, etc.).", + "tracked_object_end": { + "label": "Enviar no fim", + "description": "Envie uma solicitação à GenAI quando o objeto rastreado terminar." + }, + "after_significant_updates": { + "label": "Acionamento antecipado da GenAI", + "description": "Envie uma solicitação à GenAI após um número especificado de atualizações significativas do objeto monitorado." + } + }, + "enabled_in_config": { + "label": "Estado original da GenAI", + "description": "Indica se a GenAI foi habilitada na configuração estática original." + } + } + }, + "lpr": { + "enabled": { + "label": "Ativar LPR" + }, + "expire_time": { + "label": "Segundos para expirar", + "description": "Tempo, em segundos, após o qual uma placa não detectada é removida do rastreador (apenas para câmeras LPR dedicadas)." + }, + "min_area": { + "label": "Área mínima da placa", + "description": "Área mínima da placa (em pixels) necessária para tentar o reconhecimento." + }, + "enhancement": { + "label": "Nível de aprimoramento", + "description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela." + } + }, + "record": { + "label": "Gravação", + "enabled": { + "label": "Ativar gravação" + }, + "expire_interval": { + "label": "Intervalo de limpeza de gravação", + "description": "Minutos entre as execuções de limpeza que removem segmentos de gravação expirados." + }, + "continuous": { + "label": "Retenção contínua", + "description": "Número de dias para manter as gravações, independentemente de objetos rastreados ou movimento. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "motion": { + "label": "Retenção de movimento", + "description": "Número de dias para manter gravações acionadas por movimento, independentemente dos objetos rastreados. Defina como 0 se quiser manter apenas gravações de alertas e detecções.", + "days": { + "label": "Dias de retenção", + "description": "Dias para manter as gravações." + } + }, + "detections": { + "label": "Retenção de detecção", + "description": "Configurações de retenção de gravação para eventos de detecção, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salva todos os segmentos), motion (salva segmentos com movimento) ou active_objects (salva segmentos com objetos ativos)." + } + } + }, + "alerts": { + "label": "Retenção de alertas", + "description": "Configurações de retenção de gravações para eventos de alerta, incluindo durações de pré e pós-captura.", + "pre_capture": { + "label": "Segundos de pré-captura", + "description": "Número de segundos anteriores ao evento de detecção a serem incluídos na gravação." + }, + "post_capture": { + "label": "Segundos após a captura", + "description": "Número de segundos após o evento de detecção a serem incluídos na gravação." + }, + "retain": { + "label": "Retenção de eventos", + "description": "Configurações de retenção para gravações de eventos de detecção.", + "days": { + "label": "Dias de retenção", + "description": "Número de dias para manter as gravações de eventos de detecção." + }, + "mode": { + "label": "Modo de retenção", + "description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento) ou active_objects (salvar segmentos com objetos ativos)." + } + } + }, + "export": { + "label": "Exportar configuração", + "description": "Configurações utilizadas ao exportar gravações, como timelapse e aceleração de hardware.", + "hwaccel_args": { + "label": "Exportar argumentos de hwaccel", + "description": "Argumentos de aceleração de hardware a serem usados para operações de exportação/transcodificação." + }, + "max_concurrent": { + "label": "Máximo de exportações simultâneas", + "description": "Número máximo de tarefas de exportação a serem processadas simultaneamente." + }, + "chapters": { + "label": "Metadados de capítulos para incorporar nas gravações exportadas" + } + }, + "preview": { + "label": "Pré-visualizar configuração", + "description": "Configurações que controlam a qualidade das prévias de gravação exibidas na interface de usuário.", + "quality": { + "label": "Qualidade da pré-visualização", + "description": "Nível de qualidade da pré-visualização (muito_baixa, baixa, média, alta, muito_alta)." + } + }, + "enabled_in_config": { + "label": "Estado original da gravação", + "description": "Indica se a gravação foi habilitada na configuração estática original." + } + }, + "review": { + "label": "Rever", + "alerts": { + "label": "Configuração de alertas", + "description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.", + "enabled": { + "label": "Ativar alertas" + }, + "labels": { + "label": "Etiquetas de alerta", + "description": "Lista de rótulos de objetos que se qualificam como alertas (por exemplo: carro, pessoa)." + }, + "required_zones": { + "label": "Zonas obrigatórias", + "description": "Zonas nas quais um objeto deve entrar para ser considerado um alerta; deixe em branco para permitir qualquer zona." + }, + "enabled_in_config": { + "label": "Estado dos alertas originais", + "description": "Monitora se os alertas estavam originalmente habilitados na configuração estática." + }, + "cutoff_time": { + "label": "Horário limite dos alertas", + "description": "Segundos de espera após a ausência de atividade que gere alerta antes de encerrar o alerta." + } + }, + "detections": { + "label": "Configuração de detecções", + "description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.", + "enabled": { + "label": "Ativar detecções" + } + } } } diff --git a/web/public/locales/pt-BR/config/groups.json b/web/public/locales/pt-BR/config/groups.json index a392ecc761..cfe1a88297 100644 --- a/web/public/locales/pt-BR/config/groups.json +++ b/web/public/locales/pt-BR/config/groups.json @@ -64,5 +64,10 @@ "retention": "Retenção", "events": "Eventos" } + }, + "ffmpeg": { + "cameras": { + "cameraFfmpeg": "Argumentos do FFmpeg específicos para a câmera" + } } } diff --git a/web/public/locales/pt-BR/config/validation.json b/web/public/locales/pt-BR/config/validation.json index 3fc8086682..073d1a5785 100644 --- a/web/public/locales/pt-BR/config/validation.json +++ b/web/public/locales/pt-BR/config/validation.json @@ -28,5 +28,8 @@ "detectRequired": "Pelo menos um fluxo de entrada deve ter a função 'detectar' atribuída.", "hwaccelDetectOnly": "Somente o fluxo de entrada com a função de detecção pode definir argumentos de aceleração de hardware." } + }, + "detect": { + "dimensionMustBeEven": "Deve ser um número par." } } diff --git a/web/public/locales/pt-BR/views/chat.json b/web/public/locales/pt-BR/views/chat.json index 0967ef424b..8f63a9e1a2 100644 --- a/web/public/locales/pt-BR/views/chat.json +++ b/web/public/locales/pt-BR/views/chat.json @@ -1 +1,63 @@ -{} +{ + "documentTitle": "Frigate-Chat", + "title": "Chat-Frigate", + "subtitle": "Seu assistente de IA para gerenciamento de câmeras e insights", + "placeholder": "Pergunte o que quiser...", + "error": "Algo deu errado. Por favor, tente novamente.", + "processing": "Processando...", + "toolsUsed": "Usado: {{tools}}", + "showTools": "Mostrar ferramentas ({{count}})", + "hideTools": "Ocultar ferramentas", + "call": "Chamar", + "result": "Resultado", + "arguments": "Argumentos:", + "response": "Resposta:", + "attachment_chip_label": "{{label}} em {{camera}}", + "attachment_chip_remove": "Remover anexo", + "open_in_explore": "Abrir no Explorar", + "attach_event_aria": "Anexar evento {{eventId}}", + "attachment_picker_paste_label": "Ou cole o ID do evento", + "attachment_picker_attach": "Anexar", + "attachment_picker_placeholder": "Anexar um evento", + "quick_reply_find_similar": "Encontre avistamentos semelhantes", + "quick_reply_tell_me_more": "Conte-me mais sobre isso", + "quick_reply_when_else": "Em que outra ocasião isso foi visto?", + "quick_reply_find_similar_text": "Encontre avistamentos semelhantes a este.", + "quick_reply_tell_me_more_text": "Conte-me mais sobre este.", + "quick_reply_when_else_text": "Em que outra ocasião isso foi visto?", + "anchor": "Referência", + "similarity_score": "Semelhança", + "no_similar_objects_found": "Nenhum objeto semelhante encontrado.", + "semantic_search_required": "A busca semântica deve estar habilitada para encontrar objetos semelhantes.", + "send": "Enviar", + "suggested_requests": "Experimente perguntar:", + "starting_requests": { + "show_recent_events": "Mostrar eventos recentes", + "show_camera_status": "Mostrar status da câmera", + "recap": "O que aconteceu enquanto eu estava fora?", + "watch_camera": "Monitore uma câmera em busca de atividade" + }, + "starting_requests_prompts": { + "show_recent_events": "Mostre-me os eventos recentes da última hora", + "show_camera_status": "Qual é o status atual das minhas câmeras?", + "recap": "O que aconteceu enquanto eu estava fora?", + "watch_camera": "Fique de olho na porta da frente e me avise se alguém aparecer" + }, + "new_chat": "Novo chat", + "settings": { + "title": "Configurações do chat", + "show_stats": { + "title": "Mostrar estatísticas", + "desc": "Exibir a taxa de geração e o tamanho do contexto para respostas de chat.", + "while_generating": "Durante a geração", + "always": "Sempre" + }, + "auto_scroll": { + "title": "Rolagem automática", + "desc": "Acompanhe as novas mensagens à medida que chegam." + } + }, + "stats": { + "context": "{{tokens}} tokens" + } +} diff --git a/web/public/locales/pt-BR/views/classificationModel.json b/web/public/locales/pt-BR/views/classificationModel.json index 36f3539a04..39bc880a87 100644 --- a/web/public/locales/pt-BR/views/classificationModel.json +++ b/web/public/locales/pt-BR/views/classificationModel.json @@ -13,7 +13,7 @@ "toast": { "success": { "deletedCategory_one": "Classe Apagada", - "deletedCategory_many": "", + "deletedCategory_many": "Classes apagadas", "deletedCategory_other": "", "deletedImage_one": "Imagen Apagada", "deletedImage_many": "Imagens Apagadas", @@ -94,7 +94,14 @@ }, "noModels": { "object": { - "title": "Nenhum Modelo de Classificação de Objetos" + "title": "Nenhum Modelo de Classificação de Objetos", + "description": "Crie um modelo personalizado para classificar objetos detectados.", + "buttonText": "Criar modelo de objeto" + }, + "state": { + "title": "Modelos de Classificação Sem Estado", + "description": "Crie um modelo personalizado para monitorar e classificar mudanças de estado em áreas específicas da câmera.", + "buttonText": "Criar modelo de estado" } }, "deleteDatasetImages": { @@ -104,5 +111,13 @@ "title": "Editar Modelo de Classificação", "descriptionState": "Edite as classes deste modelo de classificação de estado. As alterações exigirão treinar novamente o modelo.", "descriptionObject": "Edite o tipo de objeto e tipo de classificação para este modelo de classificação de objeto." + }, + "reclassifyImageAs": "Reclassificar imagem como:", + "reclassifyImage": "Reclassificar imagem", + "wizard": { + "title": "Criar nova classificação", + "steps": { + "nameAndDefine": "Nomear e definir" + } } } diff --git a/web/public/locales/pt-BR/views/faceLibrary.json b/web/public/locales/pt-BR/views/faceLibrary.json index 7e8c8f56c3..9e7a5b34c2 100644 --- a/web/public/locales/pt-BR/views/faceLibrary.json +++ b/web/public/locales/pt-BR/views/faceLibrary.json @@ -72,7 +72,11 @@ "title": "Reconhecimentos Recentes", "aria": "Selecionar reconhecimentos recentes", "empty": "Não há tentativas recentes de reconhecimento facial", - "titleShort": "Recente" + "titleShort": "Recente", + "emptyNoLibrary": { + "title": "Carregue uma foto de rosto", + "description": "Você deve adicionar pelo menos um rosto à biblioteca para que o reconhecimento facial funcione." + } }, "selectFace": "Selecionar Rosto", "trainFaceAs": "Treinar Rosto como:", @@ -89,7 +93,8 @@ "renamedFace": "O rosto foi renomeado com sucesso para {{name}}", "deletedName_one": "{{count}} rosto foi deletado com sucesso.", "deletedName_many": "{{count}} rostos foram deletados com sucesso.", - "deletedName_other": "{{count}} rostos foram deletados com sucesso." + "deletedName_other": "{{count}} rostos foram deletados com sucesso.", + "reclassifiedFace": "Rosto reclassificado com sucesso." }, "error": { "uploadingImageFailed": "Falha ao enviar a imagem: {{errorMessage}}", @@ -98,7 +103,10 @@ "deleteNameFailed": "Falha ao deletar nome: {{errorMessage}}", "renameFaceFailed": "Falha ao renomear rosto: {{errorMessage}}", "trainFailed": "Falha ao treinar: {{errorMessage}}", - "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}" + "updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}", + "reclassifyFailed": "Falha ao reclassificar o rosto: {{errorMessage}}" } - } + }, + "reclassifyFaceAs": "Reclassificar rosto como:", + "reclassifyFace": "Reclassificar rosto" } diff --git a/web/public/locales/pt-BR/views/live.json b/web/public/locales/pt-BR/views/live.json index a1b72767f4..79d446d5f5 100644 --- a/web/public/locales/pt-BR/views/live.json +++ b/web/public/locales/pt-BR/views/live.json @@ -58,7 +58,9 @@ }, "camera": { "enable": "Ativar Câmera", - "disable": "Desabilitar Câmera" + "disable": "Desabilitar Câmera", + "turnOn": "Ligar a câmera", + "turnOff": "Desligar a câmera" }, "muteCameras": { "enable": "Silenciar Todas as Câmeras", @@ -70,7 +72,8 @@ }, "recording": { "enable": "Ativar Gravação", - "disable": "Desativar Gravação" + "disable": "Desativar Gravação", + "disabledInConfig": "A gravação deve ser habilitada primeiro nas configurações desta câmera." }, "snapshots": { "enable": "Permitir Capturas de Imagem", @@ -150,7 +153,8 @@ "snapshots": "Capturas de Imagem", "audioDetection": "Detecção de Áudio", "autotracking": "Auto Rastreamento", - "transcription": "Transcrição de Áudio" + "transcription": "Transcrição de Áudio", + "camera": "Câmera" }, "history": { "label": "Exibir gravação histórica" diff --git a/web/public/locales/pt-BR/views/replay.json b/web/public/locales/pt-BR/views/replay.json index 0967ef424b..58ccb6963e 100644 --- a/web/public/locales/pt-BR/views/replay.json +++ b/web/public/locales/pt-BR/views/replay.json @@ -1 +1,19 @@ -{} +{ + "title": "Depuração de Replay", + "description": "“Reproduza gravações de câmera para depuração. A lista de objetos exibe um resumo com atraso temporal, dos objetos detectados e a aba de mensagens mostra um fluxo de mensagens internas do Frigate provenientes da gravação reproduzida.", + "websocket_messages": "Mensagens", + "dialog": { + "title": "Iniciar Replay de Depuração", + "description": "Crie uma câmera de reprodução temporária que exiba imagens históricas em loop para depurar problemas de detecção e rastreamento de objetos. A câmera de reprodução terá a mesma configuração de detecção da câmera de origem. Escolha um intervalo de tempo para começar.", + "camera": "Câmera de origem", + "timeRange": "Intervalo de tempo", + "preset": { + "1m": "Último minuto", + "5m": "Últimos 5 minutos", + "timeline": "Da Linha do Tempo", + "custom": "Personalizada" + }, + "startButton": "Iniciar Reprodução", + "selectFromTimeline": "Selecionar" + } +} diff --git a/web/public/locales/pt-BR/views/settings.json b/web/public/locales/pt-BR/views/settings.json index c937906c41..00d86e28b1 100644 --- a/web/public/locales/pt-BR/views/settings.json +++ b/web/public/locales/pt-BR/views/settings.json @@ -15,7 +15,8 @@ "globalConfig": "Configuração Global - Frigate", "cameraConfig": "Configuração da Câmera - Frigate", "maintenance": "Manutenção - Frigate", - "profiles": "Perfis - Frigate" + "profiles": "Perfis - Frigate", + "detectorsAndModel": "Detectores e modelo - Frigate" }, "menu": { "ui": "UI", @@ -42,7 +43,42 @@ "globalFfmpeg": "FFmpeg", "globalMotion": "Detecção de movimento", "globalObjects": "Objetos", - "globalReview": "Revisar" + "globalReview": "Revisar", + "globalSnapshots": "Instantâneos", + "globalAudioEvents": "Detecção de áudio", + "globalLivePlayback": "Reprodução ao vivo", + "globalTimestampStyle": "Estilo de carimbo de data/hora", + "systemDatabase": "Banco de dados", + "systemTls": "TLS", + "systemAuthentication": "Autenticação", + "systemNetworking": "Rede", + "systemProxy": "Proxy", + "systemUi": "IU", + "systemLogging": "Registro", + "systemEnvironmentVariables": "Variáveis de ambiente", + "systemTelemetry": "Telemetria", + "systemBirdseye": "Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Detectores e modelo", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "Fluxo de go2rtc", + "integrationSemanticSearch": "Busca semântica", + "integrationGenerativeAi": "IA generativa", + "integrationFaceRecognition": "Reconhecimento facial", + "integrationLpr": "Reconhecimento de placas", + "integrationObjectClassification": "Classificação de objetos", + "integrationAudioTranscription": "Transcrição de áudio", + "cameraDetect": "Detecção de objetos", + "cameraFfmpeg": "Fluxos (FFmpeg)", + "cameraRecording": "Gravação", + "cameraSnapshots": "Instantâneos", + "cameraMotion": "Detecção de movimento", + "cameraObjects": "Objetos", + "cameraConfigReview": "Análise", + "cameraAudioEvents": "Detecção de áudio", + "cameraAudioTranscription": "Transcrição de áudio", + "cameraNotifications": "Notificações", + "cameraLivePlayback": "Reprodução ao vivo" }, "dialog": { "unsavedChanges": { @@ -930,6 +966,15 @@ "overriddenGlobal": "Substituir (Global)", "overriddenGlobalTooltip": "Esta câmera substitui as configurações globais desta seção", "overriddenBaseConfig": "Substituído (Configuração base)", - "overriddenBaseConfigTooltip": "O perfil {{profile}} substitui as configurações desta seção" + "overriddenBaseConfigTooltip": "O perfil {{profile}} substitui as configurações desta seção", + "overriddenBaseConfigNoDeltas": "O perfil {{profile}} substitui esta seção, mas nenhum valor de campo difere da configuração base.", + "overriddenInCameras": { + "profilePrefix": "{{profile}} perfil: {{fields}}" + } + }, + "menuDot": { + "overrideGlobal": "Esta seção substitui a configuração global", + "overrideProfile": "Esta seção é substituída pelo perfil {{profile}}", + "unsaved": "Esta seção contém alterações não salvas" } } diff --git a/web/public/locales/pt/audio.json b/web/public/locales/pt/audio.json index 3bf1ba60b4..e0a4f354f2 100644 --- a/web/public/locales/pt/audio.json +++ b/web/public/locales/pt/audio.json @@ -425,5 +425,76 @@ "chop": "Corte", "eruption": "Erupção", "environmental_noise": "Ruído Ambiental", - "field_recording": "Gravação de Campo" + "field_recording": "Gravação de Campo", + "change_ringing": "Mudar Toque", + "shofar": "Berrante", + "liquid": "Líquido", + "splash": "Respingar", + "slosh": "Respingo", + "squish": "Esmagar", + "drip": "Pingar", + "pour": "Entornar", + "trickle": "Gotejar", + "gush": "Jorrar", + "fill": "Preencher", + "spray": "Borrifar", + "pump": "Bombear", + "stir": "Mexer", + "boiling": "A ferver", + "sonar": "Sonar", + "arrow": "Flecha", + "whoosh": "Uau", + "thump": "Baque", + "thunk": "Tombo", + "electronic_tuner": "Afinador Eletrônico", + "effects_unit": "Unidade de Efeitos", + "chorus_effect": "Efeito Coro", + "basketball_bounce": "Quique da bola", + "bang": "Batida", + "slap": "Tapa", + "whack": "Bater", + "smash": "Esmagar", + "breaking": "A quebrar", + "bouncing": "A quicar", + "whip": "Chicote", + "flap": "Guia", + "scratch": "Arranhão", + "scrape": "Raspagem", + "rub": "Esfregar", + "roll": "Rolar", + "crushing": "Esmagamento", + "crumpling": "Amarrotar", + "tearing": "A rasgar", + "beep": "Bip", + "ping": "Pingo", + "ding": "Campainha", + "clang": "Estridente", + "squeal": "Guincho", + "creak": "Ranger", + "rustle": "Farfalhar", + "whir": "Zumbir", + "clatter": "Barulho", + "sizzle": "Chiado", + "clicking": "A clicar", + "clickety_clack": "Tique-taque", + "rumble": "Estrondo", + "plop": "Ploft", + "hum": "Zumbir", + "boing": "Poin", + "crunch": "Mastigar", + "sine_wave": "Onda Senoidal", + "harmonic": "Harmonica", + "chirp_tone": "Som Agudo", + "pulse": "Pulso", + "inside": "Dentro", + "outside": "Fora", + "reverberation": "Reverberação", + "echo": "Eco", + "noise": "Ruído", + "mains_hum": "Zumbido Elétrico", + "distortion": "Distorção", + "sidetone": "Retorno de Voz", + "cacophony": "Cacofonia", + "throbbing": "Latejante", + "vibration": "Vibração" } diff --git a/web/public/locales/pt/common.json b/web/public/locales/pt/common.json index 557d6b48d6..7e7e101454 100644 --- a/web/public/locales/pt/common.json +++ b/web/public/locales/pt/common.json @@ -90,7 +90,8 @@ }, "inProgress": "Em andamento", "invalidStartTime": "Horário de início inválido", - "invalidEndTime": "Horário de término inválido" + "invalidEndTime": "Horário de término inválido", + "never": "Nunca" }, "unit": { "speed": { @@ -146,7 +147,19 @@ "next": "Seguinte", "play": "Tocar", "pictureInPicture": "Imagem sobre Imagem", - "continue": "Continuar" + "continue": "Continuar", + "add": "Adicionar", + "applying": "A aplicar…", + "undo": "Desfazer", + "copiedToClipboard": "Copiado para a área de transferência", + "modified": "Modificado", + "overridden": "Substituído", + "resetToGlobal": "Redefinir para o Global", + "resetToDefault": "Redefinir para o Padrão", + "saveAll": "Gravar Tudo", + "savingAll": "A gravar tudo…", + "undoAll": "Desfazer Tudo", + "retry": "Tente novamente" }, "label": { "back": "Voltar", @@ -154,7 +167,8 @@ "show": "Exibir {{item}}", "ID": "ID", "none": "Nenhum", - "all": "Todos" + "all": "Todos", + "other": "Outros" }, "menu": { "user": { @@ -226,7 +240,10 @@ "bg": "Búlgaro", "gl": "Galego", "id": "Indonésio Bahasa", - "ur": "Urdu" + "ur": "Urdu", + "hr": "Hrvatski (Croata)", + "bs": "Bosanski (Bósnio)", + "zhHant": "繁體中文 (Chinês Tradicional)" }, "appearance": "Aparência", "darkMode": { @@ -257,7 +274,11 @@ "explore": "Explorar", "review": "Rever", "uiPlayground": "Área de Testes da IU", - "classification": "Classificação" + "classification": "Classificação", + "profiles": "Perfis", + "actions": "Ações", + "features": "Funcionalidades", + "chat": "Chat" }, "pagination": { "previous": { @@ -284,7 +305,8 @@ "error": { "noMessage": "Não foi possível guardar as alterações da configuração", "title": "Não foi possível guardar as alterações da configuração: {{errorMessage}}" - } + }, + "success": "Alterações gravadas com sucesso." } }, "accessDenied": { @@ -310,5 +332,10 @@ "field": { "optional": "Opcional", "internalID": "o Frigate utiliza o ID na configuração e no banco de dados" + }, + "no_items": "Sem elementos", + "validation_errors": "Erros de validação", + "credentialField": { + "savedPlaceholder": "Gravado - deixar em branco para manter a atual" } } diff --git a/web/public/locales/pt/components/camera.json b/web/public/locales/pt/components/camera.json index 3f7052c817..53e2853231 100644 --- a/web/public/locales/pt/components/camera.json +++ b/web/public/locales/pt/components/camera.json @@ -68,7 +68,10 @@ "stream": "Transmissão" }, "birdseye": "Vista Aérea" - } + }, + "showAll": "Mostrar todos os Grupos de Câmeras", + "showLess": "Mostrar Menos", + "editGroups": "Editar Grupos de Câmeras" }, "debug": { "options": { @@ -82,6 +85,7 @@ "zones": "Zonas", "mask": "Máscara", "motion": "Movimento", - "regions": "Regiões" + "regions": "Regiões", + "paths": "Caminhos" } } diff --git a/web/public/locales/pt/components/dialog.json b/web/public/locales/pt/components/dialog.json index 2efa3d2b5f..966c9532e2 100644 --- a/web/public/locales/pt/components/dialog.json +++ b/web/public/locales/pt/components/dialog.json @@ -36,6 +36,9 @@ "ask_a": "Este objeto é um {{label}}?", "ask_an": "Este objeto é um {{label}}?", "ask_full": "Este objeto é um {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Não foi possível submeter para Frigate+. Por favor verifique a sua connecção e tente novamente." } } }, @@ -76,6 +79,13 @@ "select": "Selecionar", "name": { "placeholder": "Nome da Exportação" + }, + "case": { + "newCaseOption": "Criar um novo caso", + "newCaseNamePlaceholder": "Nome do novo caso" + }, + "multiCamera": { + "selectFromTimeline": "Selecionar da Timeline" } }, "streaming": { diff --git a/web/public/locales/pt/views/settings.json b/web/public/locales/pt/views/settings.json index 1bab92d786..e5229ebe05 100644 --- a/web/public/locales/pt/views/settings.json +++ b/web/public/locales/pt/views/settings.json @@ -28,7 +28,8 @@ "triggers": "Gatilhos", "cameraManagement": "Gestão", "cameraReview": "Rever", - "roles": "Papéis" + "roles": "Papéis", + "profiles": "Perfis" }, "dialog": { "unsavedChanges": { diff --git a/web/public/locales/ro/config/cameras.json b/web/public/locales/ro/config/cameras.json index 9a79c555d8..7c68458241 100644 --- a/web/public/locales/ro/config/cameras.json +++ b/web/public/locales/ro/config/cameras.json @@ -29,7 +29,7 @@ }, "listen": { "label": "Tipuri ascultate", - "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, țipăt, vorbire)." + "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)." }, "filters": { "label": "Filtre audio", @@ -152,11 +152,11 @@ } }, "ffmpeg": { - "label": "FFmpeg", - "description": "Setări FFmpeg: cale binar, argumente, accelerare hardware și ieșiri per rol.", + "label": "Stream-uri (FFmpeg)", + "description": "Intrările stream-ului camerei și opțiunile FFmpeg, inclusiv calea către binar, argumentele, hwaccel și argumentele de ieșire per rol.", "path": { "label": "Cale FFmpeg", - "description": "Calea către binarul FFmpeg sau un alias de versiune (\"5.0\" sau \"7.0\")." + "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")." }, "global_args": { "label": "Argumente globale FFmpeg", diff --git a/web/public/locales/ro/config/global.json b/web/public/locales/ro/config/global.json index 40d2135cb8..1dca8a48ae 100644 --- a/web/public/locales/ro/config/global.json +++ b/web/public/locales/ro/config/global.json @@ -15,7 +15,7 @@ }, "listen": { "label": "Tipuri ascultate", - "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, țipăt, vorbire)." + "description": "Lista de evenimente audio de detectat (ex: lătrat, alarmă_incendiu, vorbire)." }, "filters": { "label": "Filtre audio", @@ -220,7 +220,7 @@ "description": "Setări FFmpeg: cale binar, argumente, accelerare hardware și ieșiri per rol.", "path": { "label": "Cale FFmpeg", - "description": "Calea către binarul FFmpeg sau un alias de versiune (\"5.0\" sau \"7.0\")." + "description": "Calea către binarul FFmpeg sau un alias de versiune (\"7.0\" sau \"8.0\")." }, "global_args": { "label": "Argumente globale FFmpeg", diff --git a/web/public/locales/ro/views/settings.json b/web/public/locales/ro/views/settings.json index b08b2f26da..c47c028377 100644 --- a/web/public/locales/ro/views/settings.json +++ b/web/public/locales/ro/views/settings.json @@ -69,7 +69,7 @@ "integrationObjectClassification": "Clasificare obiecte", "integrationAudioTranscription": "Transcriere audio", "cameraDetect": "Detecție obiecte", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "Stream-uri (FFmpeg)", "cameraRecording": "Înregistrare", "cameraSnapshots": "Snapshot-uri", "cameraMotion": "Detecție mișcare", @@ -1691,7 +1691,17 @@ } }, "cameraInputs": { - "itemTitle": "Stream-ul {{index}}" + "itemTitle": "Stream-ul {{index}}", + "sourceMode": { + "restream": "Restream (go2rtc)", + "manual": "Cale de intrare manuală", + "go2rtcStreamLabel": "Stream go2rtc", + "go2rtcStreamPlaceholder": "Selectează un stream go2rtc", + "noGo2rtcStreams": "Niciun stream go2rtc configurat", + "availableStreams": "Stream-uri disponibile", + "go2rtcStreamSearch": "Caută stream-uri...", + "noMatchingStreams": "Niciun stream corespunzător" + } }, "restartRequiredField": "Necesită repornire", "restartRequiredFooter": "Configurația a fost modificată - Necesită repornire", @@ -2085,6 +2095,9 @@ }, "onvif": { "autotrackingNoZones": "Autotracking-ul necesită cel puțin o zonă. Definește o zonă pentru această cameră în Măști / Zone, apoi seteaz-o ca zonă obligatorie mai jos." + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "Argumentele manuale pentru accelerarea hardware nu sunt recomandate. Dacă nu există o cerință specifică, selectează presetarea care se potrivește cu hardware-ul tău." } }, "birdseye": { diff --git a/web/public/locales/ro/views/system.json b/web/public/locales/ro/views/system.json index 59f52e08c4..7c0f6342f5 100644 --- a/web/public/locales/ro/views/system.json +++ b/web/public/locales/ro/views/system.json @@ -201,7 +201,22 @@ "title": "Info Sondă Cameră" }, "fps": "FPS:", - "unknown": "Necunoscut" + "unknown": "Necunoscut", + "keyframes": { + "title": "Analiză keyframe", + "analyzing": "Se analizează keyframe-urile... {{seconds}} secunde rămase", + "stillAnalyzing": "Încă se analizează keyframe-urile...", + "keyframeCount": "Keyframe-uri observate:", + "recordStream": "Stream de înregistrare:", + "observedDuration": "Durată observată:", + "gap": "Interval keyframe (min / med / max):", + "ok": "Keyframe-uri la fiecare ~{{seconds}}s, bune pentru înregistrare și redare.", + "segmentLength": "Lungime segment de înregistrare:", + "warning": "Keyframe-uri rare sau variabile (cel mai lung interval ~{{seconds}}s), probabil un codec smart (H.264+/H.265+), acest lucru nu este recomandat.", + "error": "Intervalul keyframe (~{{seconds}}s) depășește lungimea segmentului de înregistrare ({{segmentTime}}s). Unele segmente pot să nu aibă niciun keyframe, ceea ce întrerupe redarea. Dezactivează codecul smart/+ de pe cameră sau scurtează intervalul keyframe.", + "unknown": "Nu s-a putut determina distanțarea keyframe-urilor.", + "recordDisabled": "Înregistrarea este dezactivată pentru această cameră." + } }, "label": { "capture": "captură", diff --git a/web/public/locales/ru/common.json b/web/public/locales/ru/common.json index db9390ed80..5e27bc19e6 100644 --- a/web/public/locales/ru/common.json +++ b/web/public/locales/ru/common.json @@ -130,7 +130,19 @@ "export": "Экспортировать", "deleteNow": "Удалить сейчас", "next": "Следующий", - "continue": "Продолжить" + "continue": "Продолжить", + "add": "Добавить", + "applying": "Применяется…", + "undo": "Отменить", + "copiedToClipboard": "Скопировано в буфер обмена", + "saveAll": "Сохранить все", + "savingAll": "Сохранение всего…", + "undoAll": "Откатить всё", + "retry": "Повторить", + "modified": "Изменено", + "overridden": "Переопределено", + "resetToGlobal": "Сброс к Глобальному значению", + "resetToDefault": "Сброс к значению по умолчанию" }, "label": { "back": "Вернуться", @@ -169,7 +181,7 @@ "languages": "Языки", "language": { "en": "English (Английский)", - "zhCN": "简体中文 (Упрощённый китайский)", + "zhCN": "简体中文 (Упрощённый Китайский)", "es": "Español (Испанский)", "hi": "हिन्दी (Хинди)", "fr": "Français (Французский)", @@ -209,7 +221,10 @@ "bg": "Български (Болгарский)", "gl": "Galego (Галисийский)", "id": "Bahasa Indonesia (Индонезийский)", - "ur": "اردو (Урду)" + "ur": "اردو (Урду)", + "zhHant": "繁體中文 (Традиционный Китайский)", + "hr": "Hrvatski (Хорватский)", + "bs": "Bosanski (Боснийский)" }, "darkMode": { "withSystem": { @@ -261,7 +276,10 @@ }, "appearance": "Внешний вид", "classification": "Распознование", - "profiles": "Профили" + "profiles": "Профили", + "chat": "Чат", + "actions": "Действия", + "features": "Функции" }, "pagination": { "label": "пагинация", @@ -292,7 +310,8 @@ "noMessage": "Не удалось сохранить изменения конфигурации", "title": "Не удалось сохранить изменения конфигурации: {{errorMessage}}" }, - "title": "Сохранить" + "title": "Сохранить", + "success": "Изменения конфигурации сохранены успешно." } }, "role": { @@ -313,5 +332,10 @@ "field": { "optional": "Необязательный", "internalID": "Внутренний идентификатор Frigate, используемый в конфигурации и базе данных" + }, + "validation_errors": "Ошибки Валидации", + "no_items": "Нет элементов", + "credentialField": { + "savedPlaceholder": "Сохранено — оставьте пустым, чтобы сохранить текущее значение" } } diff --git a/web/public/locales/ru/components/camera.json b/web/public/locales/ru/components/camera.json index 8a8c1a4922..3ff007c1f3 100644 --- a/web/public/locales/ru/components/camera.json +++ b/web/public/locales/ru/components/camera.json @@ -2,7 +2,7 @@ "group": { "label": "Группы камер", "add": "Добавить группу камер", - "edit": "Редактирование группы камер", + "edit": "Редактировать группу камер", "delete": { "label": "Удалить группу камер", "confirm": { @@ -68,7 +68,10 @@ "placeholder": "Выбрать поток" }, "birdseye": "Birdseye" - } + }, + "showAll": "Показать все группы камер", + "showLess": "Показать меньше", + "editGroups": "Редактировать группы камер" }, "debug": { "options": { @@ -82,6 +85,7 @@ "zones": "Зоны", "mask": "Маска", "motion": "Движение", - "regions": "Регионы" + "regions": "Регионы", + "paths": "Пути" } } diff --git a/web/public/locales/ru/components/dialog.json b/web/public/locales/ru/components/dialog.json index 562e8bc088..787fa1b3f1 100644 --- a/web/public/locales/ru/components/dialog.json +++ b/web/public/locales/ru/components/dialog.json @@ -36,6 +36,9 @@ "label": "Подтвердить эту метку для Frigate Plus", "ask_a": "Это объект — {{label}}?", "ask_full": "Это объект — {{untranslatedLabel}} ({{translatedLabel}})?" + }, + "toast": { + "error": "Не удалось подключиться к Frigate+. Пожалуйста, проверьте настройки сети и попробуйте еще раз." } } }, @@ -72,7 +75,16 @@ "noVaildTimeSelected": "Не выбран допустимый временной диапазон", "endTimeMustAfterStartTime": "Время окончания должно быть после времени начала" }, - "view": "Просмотр" + "view": "Просмотр", + "queued": "Экспорт поставлен в очередь. Следите за ходом выполнения на странице экспорта.", + "batchSuccess_one": "Начат {{count}} экспорт. Открываем случай.", + "batchSuccess_few": "Начато {{count}} экспорта. Открываем случай.", + "batchSuccess_many": "Начато {{count}} экспортов. Открываем случай.", + "batchPartial": "Начато {{successful}} из {{total}} экспортов. Неудавшиеся камеры: {{failedCameras}}", + "batchFailed": "Неудавшихся экспортов: {{total}}. Неудавшиеся камеры: {{failedCameras}}", + "batchQueuedSuccess_one": "1 экспорт поставлен в очередь. Открываем случай.", + "batchQueuedSuccess_few": "{{count}} экспорта поставлено в очередь. Открываем случай.", + "batchQueuedSuccess_many": "{{count}} экспортов поставлены в очередь. Открываем случай." }, "fromTimeline": { "saveExport": "Сохранить экспорт", @@ -80,7 +92,34 @@ }, "case": { "label": "Случай", - "placeholder": "Выберите случай" + "placeholder": "Выберите случай", + "newCaseOption": "Создайте новый случай", + "newCaseNamePlaceholder": "Имя нового случая", + "newCaseDescriptionPlaceholder": "Описание Случая", + "nonAdminHelp": "Для этих экспортов будет создан новый случай." + }, + "queueing": "Помещаем Экспорт в очередь...", + "tabs": { + "export": "Одиночная Камера", + "multiCamera": "Мульти-Камера" + }, + "multiCamera": { + "timeRange": "Временной промежуток", + "selectFromTimeline": "Выбрать на таймлайне", + "cameraSelection": "Камеры", + "cameraSelectionHelp": "Камеры с отслеживаемыми объектами в этом временном диапазоне выбраны заранее", + "searchOrSelectGroup": "Найдите или выберите группу камер...", + "selectAll": "Выберите все камеры", + "clearSelection": "Очистить выбор", + "selectWithActivity": "Камеры с отслеживаемыми объектами", + "selectGroup": "Выбрать группу", + "noMatchingCameras": "Камеры по вашему запросу не найдены", + "selectedCount": "{{selected}} / {{total}} выбрано", + "checkingActivity": "Проверка доступности камеры...", + "noCameras": "Нет доступных камер", + "detectionCount_one": "{{count}} отслеживаемый объект", + "detectionCount_few": "{{count}} отслеживаемых объекта", + "detectionCount_many": "{{count}} отслеживаемых объектов" } }, "streaming": { diff --git a/web/public/locales/ru/views/settings.json b/web/public/locales/ru/views/settings.json index 8eb69ecc43..cee2a74993 100644 --- a/web/public/locales/ru/views/settings.json +++ b/web/public/locales/ru/views/settings.json @@ -16,7 +16,8 @@ "globalConfig": "Глобальная конфигурация - Frigate", "cameraConfig": "Настройки камеры - Frigate", "maintenance": "Обслуживание - Frigate", - "profiles": "Профили - Frigate" + "profiles": "Профили - Frigate", + "detectorsAndModel": "Обнаружения и модель - Frigate" }, "menu": { "cameras": "Настройки камеры", @@ -39,7 +40,37 @@ "integrations": "Интеграции", "uiSettings": "Настройки интерфейса", "profiles": "Профили", - "globalDetect": "Обнаружение объектов" + "globalDetect": "Обнаружение объектов", + "globalReview": "Обзор событий", + "globalAudioEvents": "Обнаружение аудио", + "systemAuthentication": "Аутентификация", + "systemNetworking": "Сеть", + "systemProxy": "Прокси", + "systemUi": "Интерфейс", + "systemLogging": "Логирование", + "systemEnvironmentVariables": "Переменные окружения", + "systemTelemetry": "Телеметрия", + "systemBirdseye": "Режим Birdseye", + "systemFfmpeg": "FFmpeg", + "systemDetectorsAndModel": "Детекторы и модель", + "systemMqtt": "MQTT", + "systemGo2rtcStreams": "потоки go2rtc", + "integrationSemanticSearch": "Семантический поиск", + "integrationGenerativeAi": "Генеративный ИИ", + "integrationFaceRecognition": "Распознавание лиц", + "integrationLpr": "Распознавание номерных знаков", + "integrationObjectClassification": "Классификация объектов", + "integrationAudioTranscription": "Расшифровка аудиозаписи", + "cameraDetect": "Обнаружение объектов", + "cameraFfmpeg": "Потоки (FFmpeg)", + "cameraRecording": "Запись", + "cameraSnapshots": "Снимки", + "cameraMotion": "Обнаружение движения", + "cameraObjects": "Объекты", + "cameraConfigReview": "Обзор событий", + "cameraAudioEvents": "Обнаружение аудио", + "cameraAudioTranscription": "Расшифровка аудиозаписи", + "cameraNotifications": "Уведомления" }, "dialog": { "unsavedChanges": { @@ -1201,7 +1232,28 @@ "backToSettings": "Вернуться к настройкам камеры", "streams": { "title": "Включить / Отключить камеры", - "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
Примечание: Это не отключает рестриминг go2rtc." + "desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.
Примечание: Это не отключает рестриминг go2rtc.", + "label": "Состояние камеры", + "status": { + "disabled": "Отключено" + }, + "enableSuccess": "Включена {{cameraName}}. Перезапустите Frigate для применения изменений.", + "disableSuccess": "Отключено {{cameraName}} и сохранено в конфигурации.", + "reorderHandle": "Перетяните для изменения порядка", + "saving": "Сохранение…", + "saved": "Сохранено", + "details": { + "edit": "Редактировать детали камеры", + "title": "Редактировать детали камеры", + "description": "Обновите отображаемое имя, внешний URL и видимость для этой камеры в пользовательском интерфейсе Frigate.", + "friendlyNameLabel": "Отображаемое Имя", + "friendlyNameHelp": "Дружественное имя, которое отображается для этой камеры в пользовательском интерфейсе Frigate. Оставьте пустым для использования идентификатора камеры.", + "webuiUrlLabel": "Ссылка на Web интерфейс камеры", + "webuiUrlHelp": "URL-адрес для посещения веб-UI камеры непосредственно с отладочного меню. Оставьте пустым, чтобы отключить ссылку.", + "webuiUrlInvalid": "Должен быть действительный URL (например, https://example.com).", + "dashboardLabel": "Показывать на Live dashboard", + "dashboardHelp": "Показывать эту камеру на Live dashboard." + } }, "cameraConfig": { "add": "Добавить камеру", @@ -1231,6 +1283,11 @@ "toast": { "success": "Камера {{cameraName}} успешно сохранена" } + }, + "deleteCameraDialog": { + "confirmWarning": "Удаление {{cameraName}} не может быть отменено.", + "success": "Камера {{cameraName}} успешно удалена", + "error": "Не удалось удалить камеру {{cameraName}}" } }, "cameraReview": { diff --git a/web/public/locales/sk/common.json b/web/public/locales/sk/common.json index 2a23946763..f78c9109e7 100644 --- a/web/public/locales/sk/common.json +++ b/web/public/locales/sk/common.json @@ -98,7 +98,7 @@ "kbps": "kB/s", "mbps": "MB/s", "gbps": "GB/s", - "kbph": "kb/hour", + "kbph": "kb/hodina", "mbph": "MB/hodinu", "gbph": "GB/hodinu" } @@ -149,7 +149,16 @@ "unsuspended": "Zrušte pozastavenie", "play": "Hrať", "unselect": "Zrušte výber", - "continue": "Pokračovať" + "continue": "Pokračovať", + "copiedToClipboard": "Skopírované do schránky", + "modified": "Upravené", + "overridden": "Prepísané", + "resetToGlobal": "Obnoviť na globálne", + "resetToDefault": "Obnoviť predvolené", + "saveAll": "Uložiť všetko", + "savingAll": "Ukladá sa všetko…", + "undoAll": "Vrátiť späť všetko", + "retry": "Skúste to znova" }, "menu": { "system": "Systém", @@ -252,7 +261,11 @@ }, "review": "Recenzia", "explore": "Preskúmať", - "classification": "Klasifikácia" + "classification": "Klasifikácia", + "profiles": "Profily", + "actions": "Akcie", + "features": "Vlastnosti", + "chat": "Chat" }, "toast": { "copyUrlToClipboard": "Adresa URL bola skopírovaná do schránky.", @@ -261,7 +274,8 @@ "error": { "title": "Chyba pri ukladaní zmien konfigurácie: {{errorMessage}}", "noMessage": "Chyba pri ukladaní zmien konfigurácie" - } + }, + "success": "Zmeny konfigurácie boli úspešne uložené." } }, "role": { @@ -303,5 +317,10 @@ "field": { "optional": "Voliteľné", "internalID": "Interné ID Frigate používa v konfigurácii a databáze" + }, + "no_items": "Žiadne položky", + "validation_errors": "Chyby overenia", + "credentialField": { + "savedPlaceholder": "Uložené – pre aktuálne informácie nechajte prázdne" } } diff --git a/web/public/locales/sk/components/camera.json b/web/public/locales/sk/components/camera.json index e2245bd071..0048c19e91 100644 --- a/web/public/locales/sk/components/camera.json +++ b/web/public/locales/sk/components/camera.json @@ -68,7 +68,10 @@ } }, "birdseye": "Vtáčie oko" - } + }, + "showAll": "Zobraziť všetky skupiny kamier", + "showLess": "Zobraziť menej", + "editGroups": "Upraviť skupiny kamier" }, "debug": { "options": { diff --git a/web/public/locales/sk/components/dialog.json b/web/public/locales/sk/components/dialog.json index 6904bc0d24..6eab29150d 100644 --- a/web/public/locales/sk/components/dialog.json +++ b/web/public/locales/sk/components/dialog.json @@ -6,7 +6,8 @@ "title": "Frigate sa reštartuje", "content": "Táto stránka bude obnovená o {{countdown}} sekúnd.", "button": "Vynútiť opätovné načítanie teraz" - } + }, + "description": "Počas reštartu bude Frigate nakrátko zastavený." }, "explore": { "plus": { @@ -64,6 +65,25 @@ "fromTimeline": { "saveExport": "Uložiť Export", "previewExport": "Export ukážky" + }, + "case": { + "newCaseOption": "Vytvoriť nový prípad", + "newCaseNamePlaceholder": "Názov nového prípadu", + "newCaseDescriptionPlaceholder": "Popis prípadu", + "label": "Prípad", + "nonAdminHelp": "Pre tieto exporty bude vytvorený nový prípad.", + "placeholder": "Vyberte prípad" + }, + "queueing": "Zaradenie exportu do frontu...", + "tabs": { + "export": "Jedna kamera", + "multiCamera": "Viac kamier" + }, + "multiCamera": { + "cameraSelection": "Kamery", + "cameraSelectionHelp": "Kamery so sledovanými objektmi v tomto časovom rozsahu sú vopred vybrané", + "checkingActivity": "Kontrola aktivity kamery...", + "noCameras": "Nie sú k dispozícii žiadne kamery" } }, "streaming": { diff --git a/web/public/locales/sk/config/cameras.json b/web/public/locales/sk/config/cameras.json index 0967ef424b..f6bfc32594 100644 --- a/web/public/locales/sk/config/cameras.json +++ b/web/public/locales/sk/config/cameras.json @@ -1 +1,26 @@ -{} +{ + "birdseye": { + "order": { + "label": "Pozícia", + "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye." + } + }, + "detect": { + "label": "Detekcia objektov", + "enabled": { + "label": "Povoliť detekciu objektov" + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "label": "Zistiť výstupné argumenty", + "description": "Predvolené výstupné argumenty pre detekciu streamov rolí." + }, + "record": { + "label": "Zaznamenajte výstupné argumenty", + "description": "Predvolené výstupné argumenty pre streamy rolí záznamov." + } + } + } +} diff --git a/web/public/locales/sk/config/global.json b/web/public/locales/sk/config/global.json index 0967ef424b..f6bfc32594 100644 --- a/web/public/locales/sk/config/global.json +++ b/web/public/locales/sk/config/global.json @@ -1 +1,26 @@ -{} +{ + "birdseye": { + "order": { + "label": "Pozícia", + "description": "Číselná pozícia ovládajúca poradie kamery v rozložení Birdseye." + } + }, + "detect": { + "label": "Detekcia objektov", + "enabled": { + "label": "Povoliť detekciu objektov" + } + }, + "ffmpeg": { + "output_args": { + "detect": { + "label": "Zistiť výstupné argumenty", + "description": "Predvolené výstupné argumenty pre detekciu streamov rolí." + }, + "record": { + "label": "Zaznamenajte výstupné argumenty", + "description": "Predvolené výstupné argumenty pre streamy rolí záznamov." + } + } + } +} diff --git a/web/public/locales/sk/views/classificationModel.json b/web/public/locales/sk/views/classificationModel.json index 7b5c0e59c8..8075240248 100644 --- a/web/public/locales/sk/views/classificationModel.json +++ b/web/public/locales/sk/views/classificationModel.json @@ -58,7 +58,7 @@ }, "renameCategory": { "title": "Premenovať Triedu", - "desc": "Zadajte nový názov pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila." + "desc": "Zadajte nové meno pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila." }, "description": { "invalidName": "Neplatné meno. Mená môžu obsahovať iba písmená, čísla, medzery, apostrofy, podčiarkovníky a spojovníky." diff --git a/web/public/locales/sk/views/settings.json b/web/public/locales/sk/views/settings.json index 6a451dc5ff..dbb8faf98a 100644 --- a/web/public/locales/sk/views/settings.json +++ b/web/public/locales/sk/views/settings.json @@ -18,7 +18,7 @@ "enrichments": "Obohatenia", "cameras": "Nastavenia kamier", "masksAndZones": "Masky / Zóny", - "motionTuner": "Ladič detekcie pohybu", + "motionTuner": "Ladenie detekcie pohybu", "debug": "Ladenie", "users": "Uživatelia", "notifications": "Notifikacie", @@ -26,7 +26,8 @@ "triggers": "Spúšťače", "roles": "Roly", "cameraManagement": "Manažment", - "cameraReview": "Recenzia" + "cameraReview": "Recenzia", + "profiles": "Profily" }, "dialog": { "unsavedChanges": { diff --git a/web/public/locales/sr/audio.json b/web/public/locales/sr/audio.json index fea4e3e77d..3d0bebf44e 100644 --- a/web/public/locales/sr/audio.json +++ b/web/public/locales/sr/audio.json @@ -1,24 +1,24 @@ { - "speech": "Govor", - "scream": "Vrisak", - "babbling": "Brbljanje", - "bicycle": "Bicikla", - "yell": "Vikati", - "car": "Automobil", - "bellow": "Ispod", - "motorcycle": "Motor", - "whoop": "Opa", - "whispering": "Šaptanje", - "bus": "Autobus", - "laughter": "Smeh", - "train": "Voz", - "boat": "Brod", - "crying": "Plač", + "speech": "Говор", + "scream": "Вриска", + "babbling": "Брбљање", + "bicycle": "Бицикл", + "yell": "Викање", + "car": "Аутомобил", + "bellow": "Рика", + "motorcycle": "Мотор", + "whoop": "Опа", + "whispering": "Шаптање", + "bus": "Аутобус", + "laughter": "Смех", + "train": "Воз", + "boat": "Брод", + "crying": "Плач", "sigh": "Уздах", - "singing": "Пријављивање", - "choir": "Збор", + "singing": "Певање", + "choir": "Хор", "yodeling": "Јодловање", - "chant": "Певање", + "chant": "Појање", "mantra": "Мантра", "bird": "Птица", "snicker": "Кикот", @@ -37,18 +37,18 @@ "wheeze": "Шиштање", "snoring": "Хркање", "gasp": "Задиханост", - "pant": "Задиханост", + "pant": "Дахтање", "snort": "Шмркање", "cough": "Кашаљ", "throat_clearing": "Прочишћавање грла", "sneeze": "Кијање", "sniff": "Њушкање", "run": "Трчање", - "shuffle": "Насумично", - "footsteps": "Корачање", + "shuffle": "Вучење ногама", + "footsteps": "Кораци", "chewing": "Жвакање", "biting": "Угриз", - "gargling": "Гргорење", + "gargling": "Гргљање", "stomach_rumble": "Крчање стомака", "camera": "Камера", "burping": "Подригивање", @@ -62,6 +62,442 @@ "cheering": "Навијање", "applause": "Аплауз", "chatter": "Жамор", - "crowd": "Маса", - "children_playing": "Деца се играју" + "crowd": "Гомила", + "children_playing": "Деца се играју", + "heart_murmur": "Шум на срцу", + "animal": "Животиња", + "pets": "Кућни љубимци", + "bark": "Лавеж", + "yip": "Цвиљење", + "howl": "Завијање", + "bow_wow": "Ав-ав", + "growling": "Режање", + "whimper_dog": "Цвиљење пса", + "purr": "Предење", + "meow": "Мјаукање", + "hiss": "Шиштање", + "caterwaul": "Мачје дерање", + "livestock": "Стока", + "clip_clop": "Топот копита", + "neigh": "Рзање", + "cattle": "Говеда", + "moo": "Мукање", + "cowbell": "Звоно за краве", + "pig": "Свиња", + "oink": "Рокћање", + "goat": "Коза", + "bleat": "Блејање", + "fowl": "Живина", + "chicken": "Кокошка", + "cluck": "Кокодакање", + "cock_a_doodle_doo": "Кукурикање", + "turkey": "Ћурка", + "gobble": "Глоглотање", + "duck": "Патка", + "quack": "Квакање", + "goose": "Гуска", + "honk": "Гакање", + "wild_animals": "Дивље животиње", + "roaring_cats": "Дивље мачке", + "roar": "Рика", + "chirp": "Цвркут", + "squawk": "Крештање", + "pigeon": "Голуб", + "coo": "Гукање", + "crow": "Врана", + "caw": "Грактање", + "owl": "Сова", + "hoot": "Хукање", + "flapping_wings": "Лепет крила", + "dogs": "Пси", + "rats": "Пацови", + "mouse": "Миш", + "patter": "Тапкање", + "insect": "Инсект", + "cricket": "Цврчак", + "mosquito": "Комарац", + "fly": "Мува", + "buzz": "Зујање", + "frog": "Жаба", + "croak": "Крекетање", + "snake": "Змија", + "rattle": "Звечка", + "whale_vocalization": "Вокализација кита", + "music": "Музика", + "musical_instrument": "Музички инструмент", + "plucked_string_instrument": "Жичани инструмент", + "guitar": "Гитара", + "electric_guitar": "Електрична гитара", + "bass_guitar": "Бас гитара", + "acoustic_guitar": "Акустична гитара", + "steel_guitar": "Челична гитара", + "tapping": "Тапкање", + "strum": "Брење", + "banjo": "Банџо", + "sitar": "Ситар", + "mandolin": "Мандолина", + "zither": "Цитра", + "ukulele": "Укулеле", + "keyboard": "Клавијатура", + "piano": "Клавир", + "electric_piano": "Електрични клавир", + "organ": "Оргуље", + "electronic_organ": "Електронске оргуље", + "hammond_organ": "Хамонд оргуље", + "synthesizer": "Синтисајзер", + "sampler": "Семплер", + "harpsichord": "Чембало", + "percussion": "Перкусије", + "drum_kit": "Бубањ сет", + "drum_machine": "Бубањ машина", + "drum": "Бубањ", + "snare_drum": "Мали бубањ", + "rimshot": "Ударац по обруку", + "drum_roll": "Бубањ рол", + "bass_drum": "Велики бубањ", + "timpani": "Тимпани", + "tabla": "Табла", + "cymbal": "Чинела", + "hi_hat": "Хај-хет", + "wood_block": "Дрвени блок", + "tambourine": "Даире", + "maraca": "Маракас", + "gong": "Гонг", + "tubular_bells": "Тубуларна звона", + "mallet_percussion": "Перкусије палицама", + "marimba": "Маримба", + "glockenspiel": "Глокеншпил", + "vibraphone": "Вибрафон", + "steelpan": "Стилпан", + "orchestra": "Оркестар", + "brass_instrument": "Лимени инструмент", + "french_horn": "Хорна", + "trumpet": "Труба", + "trombone": "Тромбон", + "bowed_string_instrument": "Гудачки инструмент", + "string_section": "Гудачка секција", + "violin": "Виолина", + "pizzicato": "Пицикато", + "cello": "Виолончело", + "double_bass": "Контрабас", + "wind_instrument": "Дувачки инструмент", + "flute": "Флаута", + "saxophone": "Саксофон", + "clarinet": "Кларинет", + "harp": "Харфа", + "bell": "Звоно", + "church_bell": "Црквено звоно", + "jingle_bell": "Прапорац", + "bicycle_bell": "Звоно бицикла", + "tuning_fork": "Виљушка за штимовање", + "chime": "Звоњава", + "wind_chime": "Ветрено звонце", + "harmonica": "Усна хармоника", + "accordion": "Хармоника", + "bagpipes": "Гајде", + "didgeridoo": "Диџериду", + "theremin": "Теремин", + "singing_bowl": "Тибетанска здела", + "scratching": "Скречовање", + "pop_music": "Поп музика", + "hip_hop_music": "Хип-хоп музика", + "beatboxing": "Битбоксовање", + "rock_music": "Рок музика", + "heavy_metal": "Хеви метал", + "punk_rock": "Панк рок", + "grunge": "Гранџ", + "progressive_rock": "Прогресивни рок", + "rock_and_roll": "Рок енд рол", + "psychedelic_rock": "Психоделични рок", + "rhythm_and_blues": "Ритам и блуз", + "soul_music": "Соул музика", + "reggae": "Реге", + "country": "Кантри", + "swing_music": "Свинг музика", + "bluegrass": "Блуграс", + "funk": "Фанк", + "folk_music": "Народна музика", + "middle_eastern_music": "Блискоисточна музика", + "jazz": "Џез", + "disco": "Диско", + "classical_music": "Класична музика", + "opera": "Опера", + "electronic_music": "Електронска музика", + "house_music": "Хаус музика", + "techno": "Техно", + "dubstep": "Дабстеп", + "drum_and_bass": "Драм енд бејс", + "electronica": "Електроника", + "electronic_dance_music": "Електронска плесна музика", + "ambient_music": "Амбијент музика", + "trance_music": "Транс музика", + "music_of_latin_america": "Латиноамеричка музика", + "salsa_music": "Салса музика", + "flamenco": "Фламенко", + "blues": "Блуз", + "music_for_children": "Музика за децу", + "new-age_music": "Њу ејџ музика", + "vocal_music": "Вокална музика", + "a_capella": "А капела", + "music_of_africa": "Афричка музика", + "afrobeat": "Афробит", + "christian_music": "Хришћанска музика", + "gospel_music": "Госпел музика", + "music_of_asia": "Азијска музика", + "carnatic_music": "Карнатска музика", + "music_of_bollywood": "Боливудска музика", + "ska": "Ска", + "traditional_music": "Традиционална музика", + "independent_music": "Независна музика", + "song": "Песма", + "background_music": "Позадинска музика", + "theme_music": "Тематска музика", + "jingle": "Џингл", + "soundtrack_music": "Музика филмског записа", + "lullaby": "Успаванка", + "video_game_music": "Музика видео игара", + "christmas_music": "Божићна музика", + "dance_music": "Плесна музика", + "wedding_music": "Свадбена музика", + "happy_music": "Весела музика", + "sad_music": "Тужна музика", + "tender_music": "Нежна музика", + "exciting_music": "Узбудљива музика", + "angry_music": "Бесна музика", + "scary_music": "Страшна музика", + "wind": "Ветар", + "rustling_leaves": "Шуштање лишћа", + "wind_noise": "Шум ветра", + "thunderstorm": "Олуја", + "thunder": "Гром", + "water": "Вода", + "rain": "Киша", + "raindrop": "Кап кише", + "rain_on_surface": "Киша на површини", + "stream": "Поток", + "waterfall": "Водопад", + "ocean": "Океан", + "waves": "Таласи", + "steam": "Пара", + "gurgling": "Клокотање", + "fire": "Ватра", + "crackle": "Пуцкетање", + "vehicle": "Возило", + "sailboat": "Једрилица", + "rowboat": "Чамац", + "motorboat": "Моторни чамац", + "ship": "Лађа", + "motor_vehicle": "Моторно возило", + "toot": "Труба", + "car_alarm": "Аларм аутомобила", + "power_windows": "Електрични прозори", + "skidding": "Проклизавање", + "tire_squeal": "Шкрипа гума", + "car_passing_by": "Пролазак аутомобила", + "race_car": "Тркачки аутомобил", + "truck": "Камион", + "air_brake": "Ваздушна кочница", + "air_horn": "Ваздушна сирена", + "reversing_beeps": "Звук вожње уназад", + "ice_cream_truck": "Камион за сладолед", + "emergency_vehicle": "Возило хитних служби", + "police_car": "Полицијски аутомобил", + "ambulance": "Амбулантно возило", + "fire_engine": "Ватрогасно возило", + "traffic_noise": "Шум саобраћаја", + "rail_transport": "Железнички саобраћај", + "train_whistle": "Звиждук воза", + "train_horn": "Сирена воза", + "railroad_car": "Железнички вагон", + "train_wheels_squealing": "Шкрипа точкова воза", + "subway": "Метро", + "aircraft": "Авион", + "aircraft_engine": "Мотор авиона", + "jet_engine": "Млазни мотор", + "propeller": "Пропелер", + "helicopter": "Хеликоптер", + "fixed-wing_aircraft": "Авион са фиксним крилима", + "engine": "Мотор", + "light_engine": "Лаки мотор", + "dental_drill's_drill": "Зубарска бушилица", + "lawn_mower": "Косилица", + "chainsaw": "Моторна тестера", + "medium_engine": "Средњи мотор", + "heavy_engine": "Тешки мотор", + "engine_knocking": "Лупање мотора", + "engine_starting": "Покретање мотора", + "idling": "Празан ход", + "accelerating": "Убрзавање", + "door": "Врата", + "doorbell": "Звоно на вратима", + "ding-dong": "Динг-донг", + "sliding_door": "Клизна врата", + "slam": "Лупање", + "knock": "Куцање", + "tap": "Тапкање", + "squeak": "Шкрипа", + "cupboard_open_or_close": "Отварање или затварање ормарића", + "drawer_open_or_close": "Отварање или затварање фиоке", + "dishes": "Судови", + "cutlery": "Прибор за јело", + "chopping": "Сецкање", + "frying": "Пржење", + "microwave_oven": "Микроталасна пећница", + "blender": "Блендер", + "water_tap": "Славина", + "sink": "Судопера", + "bathtub": "Када", + "hair_dryer": "Фен за косу", + "toilet_flush": "Пуштање воде", + "toothbrush": "Четкица за зубе", + "electric_toothbrush": "Електрична четкица за зубе", + "vacuum_cleaner": "Усисивач", + "zipper": "Рајсфершлус", + "keys_jangling": "Звецкање кључева", + "coin": "Новчић", + "scissors": "Маказе", + "electric_shaver": "Електрични бријач", + "shuffling_cards": "Мешање карата", + "typing": "Куцање", + "typewriter": "Писаћа машина", + "computer_keyboard": "Тастатура", + "writing": "Писање", + "alarm": "Аларм", + "telephone": "Телефон", + "telephone_bell_ringing": "Звоњава телефона", + "ringtone": "Мелодија звона", + "telephone_dialing": "Бирање броја", + "dial_tone": "Тон за бирање", + "busy_signal": "Заузет сигнал", + "alarm_clock": "Будилник", + "siren": "Сирена", + "civil_defense_siren": "Сирена цивилне заштите", + "buzzer": "Зујалица", + "smoke_detector": "Детектор дима", + "fire_alarm": "Противпожарни аларм", + "foghorn": "Сирена за маглу", + "whistle": "Звиждук", + "steam_whistle": "Парна пиштаљка", + "mechanisms": "Механизми", + "ratchet": "Чегртаљка", + "clock": "Сат", + "tick": "Откуцај", + "tick-tock": "Тик-так", + "gears": "Зупчаници", + "pulleys": "Кајише", + "sewing_machine": "Шиваћа машина", + "mechanical_fan": "Вентилатор", + "air_conditioning": "Клима уређај", + "cash_register": "Каса", + "printer": "Штампач", + "single-lens_reflex_camera": "Рефлексни фотоапарат", + "tools": "Алати", + "hammer": "Чекић", + "jackhammer": "Пнеуматска бушилица", + "sawing": "Тестерисање", + "filing": "Турпијање", + "sanding": "Брушење", + "power_tool": "Електрични алат", + "drill": "Бушилица", + "explosion": "Експлозија", + "gunshot": "Пуцањ", + "machine_gun": "Митраљез", + "fusillade": "Рафал", + "artillery_fire": "Артиљеријска паљба", + "cap_gun": "Капислара", + "fireworks": "Ватромет", + "firecracker": "Петарда", + "burst": "Прскање", + "eruption": "Ерупција", + "boom": "Бум", + "wood": "Дрво", + "chop": "Сечење", + "splinter": "Пуцање дрвета", + "crack": "Прасак", + "glass": "Стакло", + "chink": "Звецкање", + "shatter": "Разбијање", + "silence": "Тишина", + "sound_effect": "Звучни ефекат", + "environmental_noise": "Амбијентална бука", + "static": "Статички шум", + "white_noise": "Бели шум", + "pink_noise": "Розе шум", + "television": "Телевизија", + "radio": "Радио", + "field_recording": "Теренски снимак", + "sodeling": "Напевавање", + "chird": "Цвркут", + "change_ringing": "Звоњава на промену", + "shofar": "Шофар", + "liquid": "Течност", + "splash": "Пљусак", + "slosh": "Шљапкање", + "squish": "Гњечење", + "drip": "Капање", + "pour": "Сипање", + "trickle": "Цурење", + "gush": "Млаз", + "fill": "Пуњење", + "spray": "Прскање", + "pump": "Пумпа", + "stir": "Мешање", + "boiling": "Кључање", + "sonar": "Сонар", + "arrow": "Стрела", + "whoosh": "Фијук", + "thump": "Тупи ударац", + "thunk": "Тупо лупање", + "electronic_tuner": "Електронски штимер", + "effects_unit": "Јединица за ефекте", + "chorus_effect": "Хорус ефекат", + "basketball_bounce": "Одбијање лопте", + "bang": "Тресак", + "slap": "Шамар", + "whack": "Ударац", + "smash": "Лом", + "breaking": "Ломљење", + "bouncing": "Одскакање", + "whip": "Бич", + "flap": "Лепет", + "scratch": "Гребање", + "scrape": "Стругање", + "rub": "Трљање", + "roll": "Котрљање", + "crushing": "Мрвљење", + "crumpling": "Гужвање", + "tearing": "Цепање", + "beep": "Бип", + "ping": "Пинг", + "ding": "Динг", + "clang": "Звекет", + "squeal": "Цвиљење", + "creak": "Шкрипање", + "rustle": "Шуштање", + "whir": "Бруј", + "clatter": "Клопарање", + "sizzle": "Цврчање", + "clicking": "Клацкање", + "clickety_clack": "Клик-клак", + "rumble": "Тутњава", + "plop": "Пљус", + "hum": "Брујање", + "zing": "Зврндање", + "boing": "Бојнг", + "crunch": "Крцкање", + "sine_wave": "Синусни талас", + "harmonic": "Хармоник", + "chirp_tone": "Цвркут тон", + "pulse": "Пулс", + "inside": "Унутра", + "outside": "Напољу", + "reverberation": "Реверберација", + "echo": "Ехо", + "noise": "Бука", + "mains_hum": "Брујање мреже", + "distortion": "Дисторзија", + "sidetone": "Бочни тон", + "cacophony": "Какофонија", + "throbbing": "Пулсирање", + "vibration": "Вибрација" } diff --git a/web/public/locales/sr/components/dialog.json b/web/public/locales/sr/components/dialog.json index a97f7c4b6b..c4cd6401e4 100644 --- a/web/public/locales/sr/components/dialog.json +++ b/web/public/locales/sr/components/dialog.json @@ -98,7 +98,7 @@ "confirmDelete": { "title": "Потврдите брисање", "desc": { - "selected": "Да ли сте сигурни да желите да обришете све видео снимке повезане са овом ставком?

Држите притиснут Shift тастер да прескочите овај дијалог у будућности." + "selected": "Да ли сте сигурни да желите да обришете све видео снимке повезане са овом ставком?

Држите притиснут Shift тастер да прескочите овај дијалог у будућности." }, "toast": { "success": "Видео снимак повезан са изабраним ставкама за преглед успешно је обрисан.", diff --git a/web/public/locales/sr/objects.json b/web/public/locales/sr/objects.json index e6683036a6..cf99ee993a 100644 --- a/web/public/locales/sr/objects.json +++ b/web/public/locales/sr/objects.json @@ -1,12 +1,12 @@ { "person": "Osoba", - "bicycle": "Bicikla", - "car": "Automobil", - "motorcycle": "Motor", + "bicycle": "Бицикл", + "car": "Аутомобил", + "motorcycle": "Мотор", "airplane": "Avion", - "bus": "Autobus", - "train": "Voz", - "boat": "Brod", + "bus": "Аутобус", + "train": "Воз", + "boat": "Брод", "traffic_light": "Семафор", "fire_hydrant": "Хидрант", "street_sign": "Улични знак", @@ -50,5 +50,18 @@ "spoon": "Кашика", "bowl": "Посуда", "banana": "Банана", - "apple": "Јабука" + "apple": "Јабука", + "animal": "Животиња", + "bark": "Лавеж", + "goat": "Коза", + "mouse": "Миш", + "keyboard": "Клавијатура", + "vehicle": "Возило", + "door": "Врата", + "blender": "Блендер", + "sink": "Судопера", + "hair_dryer": "Фен за косу", + "toothbrush": "Четкица за зубе", + "scissors": "Маказе", + "clock": "Сат" } diff --git a/web/public/locales/sv/common.json b/web/public/locales/sv/common.json index d3f916b931..458cb287d9 100644 --- a/web/public/locales/sv/common.json +++ b/web/public/locales/sv/common.json @@ -119,7 +119,14 @@ "applying": "Verkställer…", "undo": "Ångra", "copiedToClipboard": "Kopieras till urklipp", - "modified": "Modifiera" + "modified": "Modifiera", + "overridden": "Åsidosatt", + "resetToGlobal": "Återställ till Global", + "resetToDefault": "Återställ till standard", + "saveAll": "Spara alla", + "savingAll": "Sparar alla…", + "retry": "Försök igen", + "undoAll": "Ångra alla" }, "menu": { "language": { @@ -165,7 +172,9 @@ "gl": "Galego (Galiciska)", "id": "Bahasa Indonesia (Indonesiska)", "ur": "اردو (Urdu)", - "hr": "Hrvatski (kroatiska)" + "hr": "Hrvatski (kroatiska)", + "zhHant": "繁體中文 (Traditionell kinesisk)", + "bs": "Bosanski (Bosniska)" }, "darkMode": { "withSystem": { @@ -221,7 +230,11 @@ "configurationEditor": "Konfigurationsredigerare", "withSystem": "System", "appearance": "Utseende", - "classification": "Klassificering" + "classification": "Klassificering", + "profiles": "Profiler", + "actions": "Åtgärder", + "features": "Funktioner", + "chat": "Chatt" }, "pagination": { "next": { @@ -257,7 +270,8 @@ "error": { "title": "Misslyckades med att spara konfigurationsändringar: {{errorMessage}}", "noMessage": "Misslyckades med att spara konfigurationsändringar" - } + }, + "success": "Konfigurationsändringarna har sparats." }, "copyUrlToClipboard": "Webbadressen har kopierats till urklipp." }, @@ -301,5 +315,10 @@ "field": { "optional": "Valfritt", "internalID": "Det interna ID som Frigate använder i konfigurationen och databasen" + }, + "no_items": "Inga artiklar", + "validation_errors": "Valideringsfel", + "credentialField": { + "savedPlaceholder": "Sparad — lämna tomt för att hålla dig aktuell" } } diff --git a/web/public/locales/sv/components/camera.json b/web/public/locales/sv/components/camera.json index 75fc11f82a..85996ef221 100644 --- a/web/public/locales/sv/components/camera.json +++ b/web/public/locales/sv/components/camera.json @@ -68,7 +68,10 @@ "cameras": { "desc": "Välj kameror för denna guppen.", "label": "Kameror" - } + }, + "showAll": "Visa alla kameragrupper", + "showLess": "Visa mindre", + "editGroups": "Redigera kameragrupper" }, "debug": { "options": { @@ -82,6 +85,7 @@ "zones": "Zoner", "mask": "Maskera", "motion": "Rörelse", - "regions": "Regioner" + "regions": "Regioner", + "paths": "Vägar" } } diff --git a/web/public/locales/sv/components/dialog.json b/web/public/locales/sv/components/dialog.json index d77e76a75b..5fc3794bd9 100644 --- a/web/public/locales/sv/components/dialog.json +++ b/web/public/locales/sv/components/dialog.json @@ -50,20 +50,81 @@ "placeholder": "Ge exporten ett namn" }, "select": "Välj", - "export": "Eksport", + "export": "Export", "selectOrExport": "Välj eller exportera", "toast": { "success": "Exporten har startats. Visa filen på exportsidan.", "error": { - "failed": "Misslyckades med att starta exporten: {{error}}", + "failed": "Misslyckades med att köa exporten: {{error}}", "endTimeMustAfterStartTime": "Sluttiden måste vara efter starttiden", "noVaildTimeSelected": "Inget giltigt tidsintervall valt" }, - "view": "Visa" + "view": "Visa", + "queued": "Exporten är i kö. Se förloppet på exportsidan.", + "batchSuccess_one": "Startade 1 export. Öppnar ärendet nu.", + "batchSuccess_other": "Startade {{count}} exporter. Öppnar ärendet nu.", + "batchPartial": "Startade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}", + "batchFailed": "Misslyckades med att starta export av {{total}}. Kameror som inte fungerade: {{failedCameras}}", + "batchQueuedSuccess_one": "1 export har köats. Ärendet öppnas nu.", + "batchQueuedSuccess_other": "{{count}} exporter har köats. Öppnar ärendet nu.", + "batchQueuedPartial": "Köade {{successful}} av {{total}} exporter. Misslyckade kameror: {{failedCameras}}", + "batchQueueFailed": "Misslyckades med att köa {{total}} exporter. Felaktiga kameror: {{failedCameras}}" }, "fromTimeline": { "saveExport": "Spara export", - "previewExport": "Förhandsgranska export" + "previewExport": "Förhandsgranska export", + "queueingExport": "Export i kö...", + "useThisRange": "Använd detta intervall" + }, + "case": { + "newCaseOption": "Skapa nytt ärende", + "newCaseNamePlaceholder": "Nytt ärende namn", + "newCaseDescriptionPlaceholder": "Ärende beskrivning", + "label": "Ärende", + "nonAdminHelp": "Ett nytt ärende kommer att skapas för dessa exporter.", + "placeholder": "Välj ett ärende" + }, + "queueing": "Export i kö...", + "tabs": { + "export": "Enskild kamera", + "multiCamera": "Flerkameror" + }, + "multiCamera": { + "timeRange": "Tidsintervall", + "selectFromTimeline": "Välj från tidslinjen", + "cameraSelection": "Kameror", + "cameraSelectionHelp": "Kameror med spårade objekt inom detta tidsintervall är förvalda", + "checkingActivity": "Kontrollerar kameraaktivitet...", + "noCameras": "Inga kameror tillgängliga", + "detectionCount_one": "1 spårat objekt", + "detectionCount_other": "{{count}} spårade objekt", + "nameLabel": "Exportnamn", + "namePlaceholder": "Valfritt basnamn för dessa exporter", + "queueingButton": "Köar exporter...", + "exportButton_one": "Exportera 1 kamera", + "exportButton_other": "Exportera {{count}} kameror", + "selectAll": "Välj alla kameror", + "selectWithActivity": "Kameror med spårade objekt", + "selectGroup": "Välj grupp", + "noMatchingCameras": "Ingen kamera matchar din sökning" + }, + "multi": { + "title_one": "Exportera 1 recension", + "title_other": "Exportera {{count}} recensioner", + "description": "Exportera varje vald recension. Alla exporter grupperas under ett enda ärende.", + "descriptionNoCase": "Exportera varje vald recension.", + "caseNamePlaceholder": "Granska export - {{date}}", + "exportButton_one": "Exportera 1 recension", + "exportButton_other": "Exportera {{count}} recensioner", + "exportingButton": "Exporterar...", + "toast": { + "started_one": "Startade 1 export. Öppnar ärendet nu.", + "started_other": "Startade {{count}} exporter. Öppnar ärendet nu.", + "startedNoCase_one": "Startade 1 export.", + "startedNoCase_other": "Startade {{count}} exporter.", + "partial": "Startade {{successful}} av {{total}} exporter. Misslyckades: {{failedItems}}", + "failed": "Misslyckades med att starta exporten av {{total}}. Misslyckades: {{failedItems}}" + } } }, "streaming": { @@ -111,6 +172,14 @@ "markAsReviewed": "Markera som granskad", "deleteNow": "Ta bort nu", "markAsUnreviewed": "Markera som ogranskad" + }, + "shareTimestamp": { + "label": "Dela tidsstämpel", + "title": "Dela tidsstämpel", + "description": "Dela en tidsstämplad URL för spelarens nuvarande position eller välj en anpassad tidsstämpel. Observera att detta inte är en offentlig delnings-URL och endast är tillgänglig för användare med åtkomst till Frigate och den här kameran.", + "custom": "Anpassad tidsstämpel", + "button": "Dela tidsstämpel-URL", + "shareTitle": "Frigate granskning tidsstämpel: {{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/sv/components/player.json b/web/public/locales/sv/components/player.json index 7c6301ca11..6e0cd60c35 100644 --- a/web/public/locales/sv/components/player.json +++ b/web/public/locales/sv/components/player.json @@ -4,7 +4,8 @@ "noPreviewFoundFor": "Ingen förhandsvisning hittad för {{cameraName}}", "submitFrigatePlus": { "title": "Skicka denna bild till Frigate+?", - "submit": "Skicka" + "submit": "Skicka", + "previewError": "Det gick inte att ladda förhandsgranskningen av ögonblicksbilden. Inspelningen är kanske inte tillgänglig just nu." }, "livePlayerRequiredIOSVersion": "iOS 17.1 eller senare krävs för den här typen av livestream.", "streamOffline": { @@ -47,5 +48,6 @@ "success": { "submittedFrigatePlus": "Bildruta har skickats till Frigate+ med lyckat resultat" } - } + }, + "cameraOff": "Kameran är avstängd" } diff --git a/web/public/locales/sv/config/cameras.json b/web/public/locales/sv/config/cameras.json index bfa6612cd2..6b59c41908 100644 --- a/web/public/locales/sv/config/cameras.json +++ b/web/public/locales/sv/config/cameras.json @@ -1,6 +1,29 @@ { - "label": "Kamera konfiguration", + "label": "KameraKonfiguration", "name": { - "label": "Kameranamn" + "label": "Kameranamn", + "description": "Kameranamn krävs" + }, + "friendly_name": { + "label": "Visningsnamn", + "description": "Visningsnamn för kamera i Frigate UI" + }, + "enabled": { + "label": "Aktiverad", + "description": "Aktiverad" + }, + "audio": { + "label": "Ljuddetektering", + "description": "Inställningar för ljudbaserad händelsedetektering för denna kamera.", + "enabled": { + "label": "Aktivera ljuddetektering", + "description": "Aktivera eller avaktivera ljudbaserad detektering för denna kamera." + }, + "max_not_heard": { + "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar." + }, + "min_volume": { + "label": "Minsta ljudvolym" + } } } diff --git a/web/public/locales/sv/config/global.json b/web/public/locales/sv/config/global.json index f123fa26cf..ae266a8280 100644 --- a/web/public/locales/sv/config/global.json +++ b/web/public/locales/sv/config/global.json @@ -1,5 +1,17 @@ { "version": { - "label": "Nuvarande konfigurationsversion" + "label": "Nuvarande konfigurations version" + }, + "audio": { + "label": "Ljuddetektering", + "enabled": { + "label": "Aktivera ljuddetektering" + }, + "max_not_heard": { + "description": "Antal sekunder utan den konfigurerade ljudtypen innan en ljudbaserad händelse slutar." + }, + "min_volume": { + "label": "Minsta ljudvolym" + } } } diff --git a/web/public/locales/sv/views/explore.json b/web/public/locales/sv/views/explore.json index 701f168fe9..37ce216681 100644 --- a/web/public/locales/sv/views/explore.json +++ b/web/public/locales/sv/views/explore.json @@ -289,7 +289,7 @@ "label": "Annoteringsförskjutning", "desc": "Denna data kommer från din kameras detekteringsflöde men läggs ovanpå bilder från inspelningsflödet. Det är osannolikt att de två strömmarna är helt synkroniserade. Som ett resultat kommer avgränsningsramen och filmmaterialet inte att radas upp perfekt. Du kan använda den här inställningen för att förskjuta anteckningarna framåt eller bakåt i tiden för att bättre anpassa dem till det inspelade materialet.", "millisecondsToOffset": "Millisekunder för att förskjuta detektera annoteringar med. Standard: 0", - "tips": "TIPS: Föreställ dig ett händelseklipp med en person som går från vänster till höger. Om tidslinjens avgränsningsram konsekvent är till vänster om personen bör värdet minskas. På samma sätt, om en person går från vänster till höger och avgränsningsramen konsekvent är framför personen bör värdet ökas.", + "tips": "Sänk värdet om videouppspelningen sker före rutorna och banpunkterna, och öka värdet om videouppspelningen sker bakom dem. Detta värde kan vara negativt.", "toast": { "success": "Annoteringsförskjutningen för {{camera}} har sparats i konfigurationsfilen." } diff --git a/web/public/locales/sv/views/settings.json b/web/public/locales/sv/views/settings.json index bb41cc21a4..e09673a38f 100644 --- a/web/public/locales/sv/views/settings.json +++ b/web/public/locales/sv/views/settings.json @@ -151,7 +151,8 @@ "triggers": "Utlösare", "roles": "Roller", "cameraManagement": "Hantering", - "cameraReview": "Granska" + "cameraReview": "Granska", + "profiles": "Profiler" }, "dialog": { "unsavedChanges": { @@ -729,7 +730,7 @@ "snapshotConfig": { "title": "Ögonblicksbild konfiguration", "desc": "För att skicka till Frigate+ måste ögonblicksbilder vara aktiverade i konfigurationen.", - "cleanCopyWarning": "Vissa kameror har ögonblicksbilder inaktiverade.", + "cleanCopyWarning": "Vissa kameror har ögonblicksbilder inaktiverade", "table": { "camera": "Kamera", "snapshots": "Ögonblicksbilder", diff --git a/web/public/locales/tr/common.json b/web/public/locales/tr/common.json index 08b415cede..50ea2c65f9 100644 --- a/web/public/locales/tr/common.json +++ b/web/public/locales/tr/common.json @@ -36,7 +36,7 @@ "h": "{{time}} s", "yr": "{{time}} yıl", "mo": "{{time}} ay", - "untilForTime": "{{time}}'a kadar", + "untilForTime": "{{time}} saatine kadar", "pm": "ÖS", "am": "ÖÖ", "d": "{{time}} gün", @@ -192,7 +192,9 @@ "gl": "Galego (Galiçyaca)", "id": "Bahasa Indonesia (Endonezce)", "ur": "اردو (Urduca)", - "hr": "Hrvatski (Hırvatça)" + "hr": "Hrvatski (Hırvatça)", + "zhHant": "繁體中文 (Geleneksel Çince)", + "bs": "Bosanski (Boşnakça)" }, "withSystem": "Sistem", "theme": { @@ -241,7 +243,8 @@ "classification": "Sınıflandırma", "profiles": "Profiller", "actions": "Eylemler", - "chat": "Sohbet" + "chat": "Sohbet", + "features": "Özellikler" }, "label": { "back": "Geri", @@ -324,5 +327,8 @@ "pixels": "{{area}}px" }, "no_items": "Öge bulunmuyor", - "validation_errors": "Doğrulama Hataları" + "validation_errors": "Doğrulama Hataları", + "credentialField": { + "savedPlaceholder": "Kaydedildi — mevcut haliyle kalması için boş bırakın" + } } diff --git a/web/public/locales/tr/components/camera.json b/web/public/locales/tr/components/camera.json index 7885c2653c..2076c0b9f3 100644 --- a/web/public/locales/tr/components/camera.json +++ b/web/public/locales/tr/components/camera.json @@ -68,7 +68,10 @@ "cameras": { "desc": "Bu gruba dahil olacak kameraları seçin.", "label": "Kameralar" - } + }, + "showAll": "Bütün Kamera Gruplarını Göster", + "showLess": "Daha Az", + "editGroups": "Kamera Gruplarını Düzenle" }, "debug": { "options": { @@ -82,6 +85,7 @@ "zones": "Alanlar", "mask": "Maske", "motion": "Hareket", - "regions": "Tespit Bölgeleri" + "regions": "Tespit Bölgeleri", + "paths": "Yollar" } } diff --git a/web/public/locales/tr/components/dialog.json b/web/public/locales/tr/components/dialog.json index f2c8face04..323fe5d331 100644 --- a/web/public/locales/tr/components/dialog.json +++ b/web/public/locales/tr/components/dialog.json @@ -70,12 +70,18 @@ "queued": "Dışa aktarımlar kuyruğa alındı. İlerlemeyi dışa aktarım sayfasından görebilirsiniz.", "batchSuccess_one": "1 Adet dışa aktarım başlatıldı. Durum açılıyor.", "batchSuccess_other": "{{count}} Adet dışa aktarım başlatıldı. Durum açılıyor.", - "batchPartial": "{{total}} üzerinden {{successful}} adet dışa aktarım başlatıldı. Başarısız: {{failedCameras}}" + "batchPartial": "{{total}} üzerinden {{successful}} adet dışa aktarım başlatıldı. Başarısız: {{failedCameras}}", + "batchFailed": "{{total}} dışa aktarma işlemi başlatılamadı. Başarısız kameralar: {{failedCameras}}", + "batchQueuedSuccess_one": "1 dışa aktarım sıraya alındı.", + "batchQueuedSuccess_other": "{{count}} dışa aktarım sıraya alındı.", + "batchQueuedPartial": "{{total}} dışa aktarımdan {{successful}} tanesi sıraya alındı. Başarısız olan kameralar: {{failedCameras}}", + "batchQueueFailed": "{{total}} adet dışa aktarım sıraya alınamadı. Başarısız kameralar: {{failedCameras}}" }, "fromTimeline": { "saveExport": "Dışa Aktarımı Kaydet", "previewExport": "Dışa Aktarımı Önizle", - "useThisRange": "Use This Range" + "useThisRange": "Use This Range", + "queueingExport": "Dışa aktarma sıraya alınıyor..." }, "name": { "placeholder": "Dışa Aktarımı Adlandırın" @@ -106,7 +112,14 @@ "namePlaceholder": "Dışa aktarım için opsiyonel temel isim", "queueingButton": "Dışa aktarımlar kuyruğa alınıyor...", "exportButton_one": "1 Adet Kamera Dışarı Aktarıldı", - "exportButton_other": "{{count}} Adet Kamera Dışarı Aktarıldı" + "exportButton_other": "{{count}} Adet Kamera Dışarı Aktarıldı", + "searchOrSelectGroup": "Kamera grubu arayın veya seçin...", + "selectAll": "Tüm kameraları seç", + "clearSelection": "Seçimi kaldır", + "selectWithActivity": "Nesne takibi yapan kameralar", + "selectGroup": "Grup seç", + "noMatchingCameras": "Arama kriterlerinize uyan kamera bulunamadı", + "selectedCount": "{{selected}} / {{total}} seçilen" }, "multi": { "title_one": "1 Adet Değerlendirme Dışarı Aktarıldı", @@ -172,6 +185,14 @@ "markAsReviewed": "İncelendi olarak işaretle", "deleteNow": "Şimdi Sil", "markAsUnreviewed": "Gözden geçirilmedi olarak işaretle" + }, + "shareTimestamp": { + "label": "Zaman Damgasını Paylaş", + "title": "Zaman Damgasını Paylaş", + "description": "Mevcut oynatıcı konumuna ait zaman damgalı bir URL paylaşın veya özel bir zaman damgası seçin. Bunun herkese açık bir paylaşım URL'si olmadığını ve yalnızca Frigate ile bu kameraya erişimi olan kullanıcılar tarafından görüntülenebileceğini unutmayın.", + "custom": "Özel Zaman Damgası", + "button": "Zaman Damgalı URL'yi Paylaş", + "shareTitle": "Frigate İnceleme Zaman Damgası: {{camera}}" } }, "imagePicker": { diff --git a/web/public/locales/tr/components/player.json b/web/public/locales/tr/components/player.json index 9530944695..46ff41d464 100644 --- a/web/public/locales/tr/components/player.json +++ b/web/public/locales/tr/components/player.json @@ -48,5 +48,6 @@ "title": "Bu kare Frigate+'ya gönderilsin mi?", "submit": "Gönder", "previewError": "Önizleme şuan aktif edilemiyor. Kayıt şuan mevcut olmayabilir." - } + }, + "cameraOff": "Kamera kapalı" } diff --git a/web/public/locales/tr/config/cameras.json b/web/public/locales/tr/config/cameras.json index 9ee3e04425..13595d4955 100644 --- a/web/public/locales/tr/config/cameras.json +++ b/web/public/locales/tr/config/cameras.json @@ -12,7 +12,55 @@ "description": "Etkin" }, "audio": { - "label": "Ses olayları" + "label": "Ses olayları", + "description": "Bu kamera için ses tabanlı olay algılama ayarları.", + "enabled": { + "label": "Ses algılamayı etkinleştir", + "description": "Bu kamera için ses tabanlı olay algılamayı etkinleştirin veya devre dışı bırakın." + }, + "max_not_heard": { + "label": "Zaman aşımını sonlandır", + "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi." + }, + "min_volume": { + "label": "Minimum ses seviyesi", + "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)." + }, + "listen": { + "label": "Dinlenecek ses türleri", + "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))." + }, + "filters": { + "label": "Ses filtreleri", + "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.", + "threshold": { + "label": "Minimum ses güven eşiği", + "description": "Ses olayının sayılması için gereken minimum güven eşiği." + } + }, + "enabled_in_config": { + "label": "Orijinal ses durumu", + "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir." + } }, - "label": "Kamera Konfigürasyonu" + "label": "Kamera Konfigürasyonu", + "detect": { + "stationary": { + "threshold": { + "label": "Sabitlik eşiği", + "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı." + }, + "max_frames": { + "label": "Maksimum kare sayısı", + "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.", + "default": { + "label": "Varsayılan maksimum kare sayısı", + "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı." + }, + "objects": { + "label": "Nesne maksimum kare sayısı" + } + } + } + } } diff --git a/web/public/locales/tr/config/global.json b/web/public/locales/tr/config/global.json index e86122ead5..bcc1260a2d 100644 --- a/web/public/locales/tr/config/global.json +++ b/web/public/locales/tr/config/global.json @@ -7,7 +7,34 @@ "label": "Ortam değişkenleri" }, "audio": { - "label": "Ses olayları" + "label": "Ses olayları", + "enabled": { + "label": "Ses algılamayı etkinleştir" + }, + "max_not_heard": { + "label": "Zaman aşımını sonlandır", + "description": "Ses tabanlı olayın sona ermesi için yapılandırılmış ses türünün algılanmadığı saniye süresi." + }, + "min_volume": { + "label": "Minimum ses seviyesi", + "description": "Ses algılamayı çalıştırmak için gereken minimum RMS ses seviyesi eşiği; daha düşük değerler hassasiyeti artırır (örneğin: 200 yüksek, 500 orta, 1000 düşük)." + }, + "listen": { + "label": "Dinlenecek ses türleri", + "description": "Algılanacak ses olayı türlerinin listesi (örneğin: bark (havlama), fire_alarm (yangın alarmı), speech (konuşma), yell (bağırma))." + }, + "filters": { + "label": "Ses filtreleri", + "description": "Yanlış alarmları azaltmak için kullanılan güven eşikleri gibi ses türüne özel filtre ayarları.", + "threshold": { + "label": "Minimum ses güven eşiği", + "description": "Ses olayının sayılması için gereken minimum güven eşiği." + } + }, + "enabled_in_config": { + "label": "Orijinal ses durumu", + "description": "Ses algılamanın statik yapılandırma dosyasında başlangıçta etkinleştirilip etkinleştirilmediğini belirtir." + } }, "version": { "label": "Mevcut konfigürasyon versiyonu", @@ -15,5 +42,24 @@ }, "logger": { "label": "Kayıt" + }, + "detect": { + "stationary": { + "threshold": { + "label": "Sabitlik eşiği", + "description": "Bir nesneyi sabit olarak işaretlemek için gereken, konum değişikliği olmayan kare sayısı." + }, + "max_frames": { + "label": "Maksimum kare sayısı", + "description": "Sabit nesnelerin, izlenmesi bırakılmadan önce ne kadar süre takip edileceğini sınırlar.", + "default": { + "label": "Varsayılan maksimum kare sayısı", + "description": "Sabit bir nesneyi durdurmadan önce takip etmek için varsayılan maksimum kare sayısı." + }, + "objects": { + "label": "Nesne maksimum kare sayısı" + } + } + } } } diff --git a/web/public/locales/tr/views/motionSearch.json b/web/public/locales/tr/views/motionSearch.json index 0967ef424b..2ed13c9e4c 100644 --- a/web/public/locales/tr/views/motionSearch.json +++ b/web/public/locales/tr/views/motionSearch.json @@ -1 +1,10 @@ -{} +{ + "newSearch": "Yeni Arama", + "clearResults": "Sonuçları Temizle", + "dialog": { + "cameraLabel": "Kamera" + }, + "timeRange": { + "end": "Bitiş Zamanı" + } +} diff --git a/web/public/locales/tr/views/system.json b/web/public/locales/tr/views/system.json index d388118430..8ce1835f41 100644 --- a/web/public/locales/tr/views/system.json +++ b/web/public/locales/tr/views/system.json @@ -141,7 +141,11 @@ } }, "title": "Kameralar", - "overview": "Genel" + "overview": "Genel", + "connectionQuality": { + "expectedFps": "Beklenen FPS", + "fps": "FPS" + } }, "lastRefreshed": "Son güncelleme: ", "stats": { diff --git a/web/public/locales/uk/audio.json b/web/public/locales/uk/audio.json index 773d5e3a79..4aaf0547b7 100644 --- a/web/public/locales/uk/audio.json +++ b/web/public/locales/uk/audio.json @@ -21,7 +21,7 @@ "goat": "Коза", "sheep": "Вівця", "chicken": "Курка", - "speech": "Мо́влення", + "speech": "Розмова", "idling": "Холостий хід", "railroad_car": "Залізничний вагон", "alarm": "Сигналізація", @@ -91,8 +91,8 @@ "chatter": "Балаканина", "animal": "Тварина", "yip": "Гавкання", - "babbling": "Бурмотіння", - "yell": "Кричати", + "babbling": "Балаканина", + "yell": "Крик", "bow_wow": "Гав гав", "growling": "Ревіння", "purr": "Муркотіти", diff --git a/web/public/locales/uk/common.json b/web/public/locales/uk/common.json index 937bd735bb..21002ee0cc 100644 --- a/web/public/locales/uk/common.json +++ b/web/public/locales/uk/common.json @@ -179,7 +179,8 @@ "id": "Bahasa Indonesia (Індонезійська)", "ur": "اردو (Урду)", "hr": "Hrvatski (Хорватська)", - "bs": "Bosanski (Боснійська)" + "bs": "Bosanski (Боснійська)", + "zhHant": "繁體中文 (Традиційна китайська)" }, "system": "Система", "systemMetrics": "Системна метріка", diff --git a/web/public/locales/uk/components/camera.json b/web/public/locales/uk/components/camera.json index dc12ccb8f2..c8ccbca89b 100644 --- a/web/public/locales/uk/components/camera.json +++ b/web/public/locales/uk/components/camera.json @@ -68,7 +68,10 @@ "label": "Камери", "desc": "Виберіть камери для цієї групи." }, - "icon": "Значок" + "icon": "Значок", + "showAll": "Відобразити всі групи камер", + "showLess": "Показати менше", + "editGroups": "Редагувати групи камер" }, "debug": { "zones": "Зони", diff --git a/web/public/locales/uk/components/player.json b/web/public/locales/uk/components/player.json index 300dc5fc57..7577dff25c 100644 --- a/web/public/locales/uk/components/player.json +++ b/web/public/locales/uk/components/player.json @@ -47,5 +47,6 @@ "error": { "submitFrigatePlusFailed": "Не вдалося надіслати фрейм Frigate+" } - } + }, + "cameraOff": "Камера вимкнена" } diff --git a/web/public/locales/uk/config/cameras.json b/web/public/locales/uk/config/cameras.json index c0be2e59ac..4de1a14df5 100644 --- a/web/public/locales/uk/config/cameras.json +++ b/web/public/locales/uk/config/cameras.json @@ -11,5 +11,62 @@ "enabled": { "label": "Увімкнено", "description": "Увімкнено" + }, + "audio": { + "label": "Виявлення звуку", + "enabled": { + "label": "Увімкнути виявлення звуку", + "description": "Увімкнути або вимкнути виявлення звукових подій для цієї камери." + }, + "max_not_heard": { + "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.", + "label": "Кінець очікування" + }, + "min_volume": { + "label": "Мінімальний рівень звуку", + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "num_threads": { + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "listen": { + "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).", + "label": "Типи звукових подій" + }, + "filters": { + "label": "Фільтри звуку" + }, + "description": "Налаштування звукових подій для цієї камери." + }, + "audio_transcription": { + "label": "Транскрипція аудіо", + "enabled": { + "label": "Увімкнути транскрипцію" + }, + "live_enabled": { + "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить." + } + }, + "birdseye": { + "label": "Режим Birdseye", + "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.", + "enabled": { + "label": "Увімкнути Birdseye", + "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye." + }, + "mode": { + "label": "Режим відстежування", + "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)." + }, + "order": { + "label": "Розміщення", + "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye." + } + }, + "detect": { + "label": "Виявлення об'єктів", + "enabled": { + "label": "Увімкнути виявлення об'єктів" + } } } diff --git a/web/public/locales/uk/config/global.json b/web/public/locales/uk/config/global.json index 0967ef424b..6618f81f66 100644 --- a/web/public/locales/uk/config/global.json +++ b/web/public/locales/uk/config/global.json @@ -1 +1,54 @@ -{} +{ + "audio": { + "label": "Виявлення звуку", + "enabled": { + "label": "Увімкнути виявлення звуку" + }, + "max_not_heard": { + "description": "Кількість секунд без налаштованого типу звуку перед тим, як звукова подія закінчиться.", + "label": "Кінець очікування" + }, + "min_volume": { + "label": "Мінімальний рівень звуку", + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "num_threads": { + "description": "Поріг мінімального середньоквадратичного значення необхідний для запуску розпізнавання звуку; чим нижче значення, тим вища чутливість (наприклад, 200 — висока, 500 — середня, 1000 — низька)." + }, + "listen": { + "description": "Список звукових подій для виявлення (наприклад, bark, fire_alarm, speech, yell).", + "label": "Типи звукових подій" + }, + "filters": { + "label": "Фільтри звуку" + } + }, + "audio_transcription": { + "label": "Транскрипція аудіо", + "live_enabled": { + "description": "Увімкнути потокову транскрипцію аудіо в реальному часі коли вона надходить." + } + }, + "birdseye": { + "label": "Режим Birdseye", + "description": "Налаштування для складеного елементу перегляду режиму Birdseye який відображає потоки з декількох камер на єдиному макеті.", + "enabled": { + "label": "Увімкнути Birdseye", + "description": "Увімкнути або вимкнути функцію перегляду у режимі Birdseye." + }, + "mode": { + "label": "Режим відстежування", + "description": "Режими для додавання камер в Birdseye: 'objects' (об'єкти), 'motion' (рух) або 'continuous' (безперервний)." + }, + "order": { + "label": "Розміщення", + "description": "Номер позиції, що визначає розміщення камери на макеті Birdseye." + } + }, + "detect": { + "label": "Виявлення об'єктів", + "enabled": { + "label": "Увімкнути виявлення об'єктів" + } + } +} diff --git a/web/public/locales/uk/views/system.json b/web/public/locales/uk/views/system.json index a503559ed2..4d8412f44f 100644 --- a/web/public/locales/uk/views/system.json +++ b/web/public/locales/uk/views/system.json @@ -40,7 +40,10 @@ "tips": { "title": "Інформація про зонд камери" }, - "aspectRatio": "співвідношення сторін" + "aspectRatio": "співвідношення сторін", + "keyframes": { + "observedDuration": "Тривалість спостереження:" + } }, "overview": "Огляд", "framesAndDetections": "Кадри / Виявлення" @@ -178,7 +181,8 @@ "logs": { "frigate": "Фрегатні журнали - Фрегат", "go2rtc": "Журнали Go2RTC - Фрегат", - "nginx": "Журнали Nginx - Фрегат" + "nginx": "Журнали Nginx - Фрегат", + "websocket": "Журнал повідомлень - Frigate" } }, "title": "Система", @@ -204,6 +208,25 @@ "fetchingLogsFailed": "Помилка отримання журналів: {{errorMessage}}", "whileStreamingLogs": "Помилка під час потокової передачі журналів: {{errorMessage}}" } + }, + "websocket": { + "label": "Повідомлення", + "pause": "Павза", + "resume": "Продовжити", + "clear": "Очистити", + "filter": { + "all": "Всі теми (topics)", + "topics": "Теми (topics)", + "events": "Події", + "reviews": "Перевірки", + "classification": "Класифікація", + "face_recognition": "Розпізнавання обличчя", + "lpr": "Розпізнавання номерних знаків (LPR)", + "camera_activity": "Активність камери", + "system": "Система", + "camera": "Камера", + "all_cameras": "Всі камери" + } } } } diff --git a/web/public/locales/zh-CN/components/camera.json b/web/public/locales/zh-CN/components/camera.json index 9bd70155e4..1e45e408d6 100644 --- a/web/public/locales/zh-CN/components/camera.json +++ b/web/public/locales/zh-CN/components/camera.json @@ -68,7 +68,10 @@ "placeholder": "选择视频流" }, "birdseye": "鸟瞰图" - } + }, + "showAll": "显示所有的摄像头组摄像头摄像头", + "showLess": "显示更少", + "editGroups": "编辑摄像头组" }, "debug": { "options": { diff --git a/web/public/locales/zh-CN/components/dialog.json b/web/public/locales/zh-CN/components/dialog.json index 1cab8c0f82..35e45b7926 100644 --- a/web/public/locales/zh-CN/components/dialog.json +++ b/web/public/locales/zh-CN/components/dialog.json @@ -32,6 +32,9 @@ "ask_a": "这个目标/物体是 {{label}} 吗?", "ask_an": "这个目标/物体是 {{label}} 吗?", "ask_full": "这个目标/物体是 {{untranslatedLabel}} ({{translatedLabel}}) 吗?" + }, + "toast": { + "error": "提交至 Frigate+ 失败,请检查您的网络连接并重试。" } } }, @@ -105,7 +108,14 @@ "nameLabel": "导出名称", "namePlaceholder": "这些导出文件的可选基础名称", "queueingButton": "正在加入导出队列…", - "exportButton_other": "导出 {{count}} 个摄像头" + "exportButton_other": "导出 {{count}} 个摄像头", + "searchOrSelectGroup": "搜索或选择摄像头组…", + "selectAll": "选择所有摄像头", + "clearSelection": "清空选择", + "selectWithActivity": "包含追踪目标的摄像头", + "selectGroup": "选择组", + "noMatchingCameras": "没有匹配搜索的摄像头", + "selectedCount": "{{selected}} / {{total}} 个" }, "multi": { "title_other": "导出 {{count}} 个核查", diff --git a/web/public/locales/zh-CN/config/cameras.json b/web/public/locales/zh-CN/config/cameras.json index 9faae8a747..93d82d6825 100644 --- a/web/public/locales/zh-CN/config/cameras.json +++ b/web/public/locales/zh-CN/config/cameras.json @@ -33,7 +33,7 @@ }, "listen": { "label": "监听类型", - "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、scream、speech、yell)。" + "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。" }, "filters": { "label": "音频过滤器", @@ -152,11 +152,11 @@ } }, "ffmpeg": { - "label": "FFmpeg", - "description": "FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速选项,以及按不同功能划分的输出参数。", + "label": "视频流(FFmpeg)", + "description": "摄像头视频流和 FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速,以及按不同功能划分的输出参数。", "path": { "label": "FFmpeg 路径", - "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"5.0\" 或 \"7.0\")。" + "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。" }, "global_args": { "label": "FFmpeg 全局参数", @@ -524,6 +524,9 @@ "max_concurrent": { "label": "最大并发导出数", "description": "同时可处理的最大导出任务数量。" + }, + "chapters": { + "label": "导出的录制中嵌入元数据" } }, "preview": { diff --git a/web/public/locales/zh-CN/config/global.json b/web/public/locales/zh-CN/config/global.json index 4e35be178b..96f45f9601 100644 --- a/web/public/locales/zh-CN/config/global.json +++ b/web/public/locales/zh-CN/config/global.json @@ -44,7 +44,7 @@ }, "listen": { "label": "监听类型", - "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、scream、speech、yell)。" + "description": "要检测的音频事件类型列表(例如:bark、fire_alarm、speech、yell)。" }, "filters": { "label": "音频过滤器", @@ -292,7 +292,7 @@ "description": "FFmpeg 编解码相关设置,包含可执行文件路径、命令行参数、硬件加速选项,以及按不同功能划分的输出参数。", "path": { "label": "FFmpeg 路径", - "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"5.0\" 或 \"7.0\")。" + "description": "要使用的 FFmpeg 可执行文件路径,或版本别名(如 \"7.0\" 或 \"8.0\")。" }, "global_args": { "label": "FFmpeg 全局参数", @@ -1778,6 +1778,9 @@ "max_concurrent": { "label": "最大并发导出数", "description": "同时可处理的最大导出任务数量。" + }, + "chapters": { + "label": "导出的录制中嵌入元数据" } }, "preview": { diff --git a/web/public/locales/zh-CN/views/search.json b/web/public/locales/zh-CN/views/search.json index 51fe47c8ef..ffb770839c 100644 --- a/web/public/locales/zh-CN/views/search.json +++ b/web/public/locales/zh-CN/views/search.json @@ -46,7 +46,7 @@ "tips": { "title": "如何使用文本筛选器", "desc": { - "text": "筛选器可帮助您缩小搜索范围。注意,目前还暂不支持中文搜索。以下是在输入字段中使用筛选器的方法:", + "text": "筛选器可帮助您缩小搜索范围。注意,Jina v1 不支持中文搜索。以下是在输入字段中使用筛选器的方法:", "step": "
  • 输入筛选器名称后跟一个冒号(例如:“cameras:”)。
  • 从建议中选择一个值或输入您自己的值。
  • 使用多个筛选器时,可以在它们之间用空格分隔。
  • 日期筛选器(before: 和 after:)使用 {{DateFormat}} 格式。
  • 时间范围筛选器使用 {{exampleTime}} 格式。
  • 点击筛选器旁边的“x”即可移除筛选条件。
", "example": "示例:cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM", "step2": "选择给出的建议值或自行输入;", diff --git a/web/public/locales/zh-CN/views/settings.json b/web/public/locales/zh-CN/views/settings.json index aa7904a6ca..9a6dbc592c 100644 --- a/web/public/locales/zh-CN/views/settings.json +++ b/web/public/locales/zh-CN/views/settings.json @@ -70,7 +70,7 @@ "integrationObjectClassification": "目标分类", "integrationAudioTranscription": "音频转录", "cameraDetect": "目标检测", - "cameraFfmpeg": "FFmpeg", + "cameraFfmpeg": "视频流(FFmpeg)", "cameraRecording": "录制", "cameraSnapshots": "快照", "cameraMotion": "画面变动检测", @@ -1700,7 +1700,17 @@ } }, "cameraInputs": { - "itemTitle": "视频流 {{index}}" + "itemTitle": "视频流 {{index}}", + "sourceMode": { + "restream": "转流传输(go2rtc)", + "manual": "手动输入路径", + "go2rtcStreamPlaceholder": "选择 go2rtc 视频流", + "noGo2rtcStreams": "没有配置 go2rtc 视频流", + "go2rtcStreamSearch": "搜索视频流…", + "go2rtcStreamLabel": "go2rtc 视频流", + "availableStreams": "可用的视频流", + "noMatchingStreams": "没有匹配的视频流" + } }, "restartRequiredField": "需要重启", "restartRequiredFooter": "配置已更改 - 需要重启", @@ -2082,7 +2092,11 @@ "fpsGreaterThanFive": "不建议设置检测帧率高于 5,数值设置过高可能引发性能问题,且不会带来任何增益。", "disabled": "目标检测已禁用。快照、回放条目以及人脸识别、车牌识别、生成式 AI 等增强功能都将无法使用。", "resolutionShouldBeMultipleOfFour": "为了获得最佳效果,检测的宽度和高度应该是4的倍数。其他偶数值可能会在检测流中产生视觉伪影或轻微失真。", - "aspectRatioMismatch": "你输入的宽度和高度与当前检测分辨率的不匹配,这可能会导致图像被拉伸或变形。" + "aspectRatioMismatch": "你输入的宽度和高度与当前检测分辨率的不匹配,这可能会导致图像被拉伸或变形。", + "maxFramesSet": "设置最大帧数将会覆盖默认行为,并且禁用静态物体追踪。只有很少的情况下需要这个功能,请谨慎使用。", + "squareResolution": "方形的检测分辨率比较少见。检测宽度和高度应该与你摄像头的画面比例一致(例如 16:9),而不是跟目标检测模型的输入尺寸一致。画面比例不匹配可能会拉伸图像,从而降低检测准确率。", + "resolutionHigh": "这个检测分辨率高于推荐值,可能会增加资源占用,但不一定提升检测准确率。对于大多数摄像头,建议使用 1080p 或更低的检测分辨率。", + "globalResolutionMultipleCameras": "在配置了多个摄像头同时还设置了全局检测分辨率。除非所有摄像头都使用相同的分辨率和画面比例,否则应该为每个摄像头单独设置检测宽度和高度,使其匹配各自摄像头的原生画面比例。" }, "faceRecognition": { "globalDisabled": "必须开启人脸识别增强功能,此摄像头的人脸识别相关功能才能正常使用。", @@ -2111,6 +2125,13 @@ }, "onvif": { "autotrackingNoZones": "自动追踪至少需要一个区域。请先在“遮罩 / 区域”中为此摄像头定义一个区域,然后在下方将其设置为必需区域。" + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "不建议手动硬件加速参数。除非存在特定需求,否则选择与你的硬件匹配的预设。" + }, + "model": { + "optimizedFor320": "Frigate 针对了 320x320 的模型进行优化,这是大多数设置的最佳选择。640x640 模型速度较慢,仅在特定场景下有帮助。", + "inputDimensionsNotDetectResolution": "模型输入的宽度和高度是目标检测模型的输入尺寸,而不是你摄像头的检测分辨率。它们应该与你正在使用的模型的尺寸相匹配,通常是一个方形尺寸,如320x320或640x640。" } }, "birdseye": { @@ -2172,7 +2193,7 @@ }, "retainMode": { "all": "全部", - "motion": "运动", + "motion": "画面变动", "active_objects": "活动目标" }, "previewQuality": { diff --git a/web/public/locales/zh-CN/views/system.json b/web/public/locales/zh-CN/views/system.json index 79882b6afe..c14f062916 100644 --- a/web/public/locales/zh-CN/views/system.json +++ b/web/public/locales/zh-CN/views/system.json @@ -175,7 +175,22 @@ "tips": { "title": "摄像头信息" }, - "aspectRatio": "宽高比" + "aspectRatio": "宽高比", + "keyframes": { + "title": "关键帧分析", + "analyzing": "正在分析关键帧... 剩余 {{seconds}} 秒", + "stillAnalyzing": "仍在分析关键帧...", + "recordStream": "录制视频流:", + "keyframeCount": "观察到的关键帧:", + "observedDuration": "观测持续时间:", + "gap": "关键帧间隔(最小值 / 平均值 / 最大值):", + "segmentLength": "录制片段长度:", + "ok": "每 {{seconds}} 秒取一帧,适用于录制和回放。", + "warning": "稀疏或不均匀的关键帧(最长间隔约{{seconds}}秒),可能是使用了智能编码器(例如H.264+/H.265+),不建议开启该功能。", + "error": "关键帧间隔(~{{seconds}}秒)超过了录制片段长度({{segmentTime}}秒)。某些片段可能没有关键帧,这会导致播放中断。请尝试禁用摄像头的智能或 + 编解码器或缩短其关键帧间隔。", + "unknown": "无法确定关键帧间隔。", + "recordDisabled": "此摄像头的录制功能已禁用。" + } }, "framesAndDetections": "帧数/检测次数", "label": { diff --git a/web/public/locales/zh-Hant/common.json b/web/public/locales/zh-Hant/common.json index 31ee3313e9..08ae3d639f 100644 --- a/web/public/locales/zh-Hant/common.json +++ b/web/public/locales/zh-Hant/common.json @@ -70,7 +70,7 @@ "inProgress": "處理中", "invalidStartTime": "無效的起始時間", "invalidEndTime": "無效的結束時間", - "never": "從不" + "never": "永不" }, "unit": { "speed": { @@ -307,7 +307,7 @@ "internalID": "在Frigate 設定檔和資料庫使用的內部ID" }, "information": { - "pixels": "{area}}像素" + "pixels": "{{area}} 像素" }, "no_items": "沒有項目", "validation_errors": "驗證錯誤", diff --git a/web/public/locales/zh-Hant/components/dialog.json b/web/public/locales/zh-Hant/components/dialog.json index 3d6f33a684..4d294718c2 100644 --- a/web/public/locales/zh-Hant/components/dialog.json +++ b/web/public/locales/zh-Hant/components/dialog.json @@ -20,6 +20,9 @@ }, "state": { "submitted": "已提交" + }, + "toast": { + "error": "無法提交至 Frigate+,請檢查您的網路連線並再試一次。" } }, "submitToPlus": { @@ -97,7 +100,14 @@ "nameLabel": "匯出名稱", "namePlaceholder": "這些匯出檔案的可選基礎名稱", "queueingButton": "正在加入匯出佇列…", - "exportButton_other": "匯出 {{count}} 個攝影機" + "exportButton_other": "匯出 {{count}} 個攝影機", + "searchOrSelectGroup": "搜尋或選擇一個攝影機群組…", + "selectAll": "選擇所有攝影機", + "clearSelection": "清除選擇", + "selectWithActivity": "有追蹤物件的攝影機", + "selectGroup": "選擇群組", + "noMatchingCameras": "找不到符合您搜尋條件的攝影機", + "selectedCount": "已選取 {{selected}} / {{total}}" }, "multi": { "title_other": "匯出 {{count}} 個審閱", @@ -147,7 +157,7 @@ "confirmDelete": { "title": "確認刪除", "desc": { - "selected": "你確定要刪除所有與此查核項目有關的錄影檔案嗎?

按住Shift以在未來跳過此確認步驟。" + "selected": "您確定要刪除所有與此審查項目相關的錄影檔案嗎?

按住 Shift 鍵可在未來跳過此確認步驟。" }, "toast": { "success": "已成功刪除與選擇的審核物件有關的影片片段。", diff --git a/web/public/locales/zh-Hant/components/filter.json b/web/public/locales/zh-Hant/components/filter.json index 1cbef2fd31..1b3cbe0dc7 100644 --- a/web/public/locales/zh-Hant/components/filter.json +++ b/web/public/locales/zh-Hant/components/filter.json @@ -106,7 +106,7 @@ }, "trackedObjectDelete": { "title": "確認刪除", - "desc": "刪除這 {{objectLength}} 個追蹤物件將會刪除截圖、儲存的嵌入資料,以及相關的物件生命週期紀錄。歷史記錄中的錄影檔案不會被刪除。

你確定要繼續嗎?

按住 Shift 可在未來跳過這個確認內容。", + "desc": "刪除這 {{objectLength}} 個追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的物件生命週期紀錄。歷史檢視中的錄影檔案 不會 被刪除。

確定要繼續嗎?

按住 Shift 鍵可在未來跳過此對話框。", "toast": { "success": "成功刪除追蹤物件。", "error": "刪除追蹤物件失敗:{{errorMessage}}" diff --git a/web/public/locales/zh-Hant/config/cameras.json b/web/public/locales/zh-Hant/config/cameras.json index 1b989699af..b9c9244212 100644 --- a/web/public/locales/zh-Hant/config/cameras.json +++ b/web/public/locales/zh-Hant/config/cameras.json @@ -697,7 +697,7 @@ "description": "在接收到音訊時開啟即時監控持續轉寫。" }, "enabled": { - "label": "開啟轉錄", + "label": "啟用語音轉文字", "description": "開啟或關閉手動觸發的音訊事件轉寫。" }, "enabled_in_config": { @@ -857,7 +857,7 @@ "description": "攝影機型別" }, "ui": { - "label": "攝影機頁面", + "label": "攝影機介面", "description": "此攝影機在頁面中的顯示順序和可見性。顯示順序僅影響預設儀表板。如需更精細的控制,請使用“攝影機組”。", "order": { "label": "UI 順序", diff --git a/web/public/locales/zh-Hant/config/global.json b/web/public/locales/zh-Hant/config/global.json index 258eb17608..a71d8c6210 100644 --- a/web/public/locales/zh-Hant/config/global.json +++ b/web/public/locales/zh-Hant/config/global.json @@ -389,7 +389,7 @@ "label": "DeepStack", "description": "將影像傳送到遠端 DeepStack HTTP API 進行推理的 DeepStack/CodeProject.AI 偵測器。不推薦使用。", "api_url": { - "label": "DeepStack API URL", + "label": "DeepStack API 網址", "description": "DeepStack API 的 URL。" }, "api_timeout": { @@ -756,7 +756,7 @@ }, "live": { "label": "即時監控觀看", - "description": "用於控制 JSMPEG 即時流解析度與畫質的設定。此設定不影響使用 go2rtc 進行即時預覽的攝影機。", + "description": "用於控制 jsmpeg 即時串流解析度與畫質的設定。此設定不影響使用 go2rtc 進行即時預覽的重新串流攝影機。", "streams": { "label": "即時監控流名稱", "description": "配置的流名稱到用於即時監控播放的 restream/go2rtc 名稱的對映。" @@ -946,7 +946,15 @@ "description": "判定屬性有效所需的平均偵測置信度門檻。" }, "min_score": { - "label": "最小置信度" + "label": "最小置信度", + "description": "此屬性與其父物件關聯所需的最小單幀畫面的偵測置信度。" + }, + "mask": { + "label": "篩選遮罩", + "description": "定義此篩選器在畫面內應用位置的多邊形座標。" + }, + "raw_mask": { + "label": "原始遮罩" } } }, @@ -1230,7 +1238,7 @@ "label": "音訊轉錄", "description": "用於事件和即時字幕的即時和語音音訊轉錄設定。", "enabled": { - "label": "開啟音訊轉錄", + "label": "啟用語音轉文字", "description": "為所有攝影機啟用或停用自動音訊轉錄;可按攝影機覆蓋。" }, "language": { @@ -1546,6 +1554,10 @@ "dashboard": { "label": "在 UI 中顯示", "description": "切換此攝影機在 Frigate 頁面中是否可見。停用後需要手動編輯配置才能再次在頁面中檢視此攝影機。" + }, + "review": { + "label": "在審查中顯示", + "description": "切換此攝影機在審查中是否顯示(包含審查頁面及其攝影機篩選器、動態審查和歷史檢視)。" } }, "onvif": { diff --git a/web/public/locales/zh-Hant/config/validation.json b/web/public/locales/zh-Hant/config/validation.json index 2c0274b3b6..c75f681df8 100644 --- a/web/public/locales/zh-Hant/config/validation.json +++ b/web/public/locales/zh-Hant/config/validation.json @@ -28,5 +28,8 @@ "detectRequired": "必須至少有一個輸入串流分配為 'detect' 角色。", "hwaccelDetectOnly": "只有分配了 detect 角色的輸入串流才能定義硬體加速引數。" } + }, + "detect": { + "dimensionMustBeEven": "必須是偶數。" } } diff --git a/web/public/locales/zh-Hant/views/chat.json b/web/public/locales/zh-Hant/views/chat.json index fced43e35e..af68b79f56 100644 --- a/web/public/locales/zh-Hant/views/chat.json +++ b/web/public/locales/zh-Hant/views/chat.json @@ -60,5 +60,13 @@ "stats": { "context": "{{tokens}} 個 token", "tokens_per_second": "{{rate}} tokens/秒" + }, + "reasoning": { + "active": "推論中…", + "show": "顯示推論過程", + "hide": "隱藏推論過程" + }, + "thinking": { + "toggle": "切換顯示推論" } } diff --git a/web/public/locales/zh-Hant/views/classificationModel.json b/web/public/locales/zh-Hant/views/classificationModel.json index 6c0a1d9651..c716e89444 100644 --- a/web/public/locales/zh-Hant/views/classificationModel.json +++ b/web/public/locales/zh-Hant/views/classificationModel.json @@ -1,9 +1,9 @@ { "toast": { "success": { - "deletedImage_other": "已刪除的圖片", + "deletedImage_other": "已刪除 {{count}} 張圖片", "deletedModel_other": "已成功刪除 {{count}} 個模型", - "deletedCategory_other": "已刪除分類", + "deletedCategory_other": "已刪除 {{count}} 個類別", "categorizedImage": "成功分類圖片", "trainedModel": "訓練模型成功。", "trainingModel": "已開始模型訓練。", diff --git a/web/public/locales/zh-Hant/views/explore.json b/web/public/locales/zh-Hant/views/explore.json index 671e9201bf..6fa668056d 100644 --- a/web/public/locales/zh-Hant/views/explore.json +++ b/web/public/locales/zh-Hant/views/explore.json @@ -234,7 +234,7 @@ "dialog": { "confirmDelete": { "title": "確認刪除", - "desc": "刪除此追蹤物件將移除截圖、所有已保存的嵌入,以及所有相關的追蹤詳情。歷史記錄中的錄影不會被刪除。

你確定要刪除嗎?" + "desc": "刪除此追蹤物件將會移除快照、已儲存的嵌入向量資料(embeddings),以及相關的追蹤詳情紀錄。歷史檢視中的錄影檔案 不會 被刪除。

確定要刪除嗎?" }, "toast": { "error": "刪除該追蹤目標時出錯:{{errorMessage}}" diff --git a/web/public/locales/zh-Hant/views/live.json b/web/public/locales/zh-Hant/views/live.json index d1e28743fd..cb4ed938ee 100644 --- a/web/public/locales/zh-Hant/views/live.json +++ b/web/public/locales/zh-Hant/views/live.json @@ -58,7 +58,9 @@ }, "camera": { "enable": "啟用鏡頭", - "disable": "停用鏡頭" + "disable": "停用鏡頭", + "turnOn": "開啟攝影機", + "turnOff": "關閉攝影機" }, "muteCameras": { "enable": "所有鏡頭靜音", @@ -151,7 +153,8 @@ "snapshots": "截圖", "audioDetection": "音訊偵測", "autotracking": "自動追蹤", - "transcription": "音訊轉錄" + "transcription": "音訊轉錄", + "camera": "攝影機" }, "history": { "label": "顯示歷史影像" diff --git a/web/public/locales/zh-Hant/views/motionSearch.json b/web/public/locales/zh-Hant/views/motionSearch.json index a83835afa0..7ea301ccac 100644 --- a/web/public/locales/zh-Hant/views/motionSearch.json +++ b/web/public/locales/zh-Hant/views/motionSearch.json @@ -22,7 +22,9 @@ "polygonControls": { "points_other": "{{count}} 個點位", "undo": "撤銷上一個點位", - "reset": "重設多邊形" + "reset": "重設多邊形", + "drawMode": "繪製", + "moveMode": "移動" }, "motionHeatmapLabel": "畫面變動熱力圖", "dialog": { @@ -38,7 +40,7 @@ "settings": { "title": "搜尋設定", "parallelMode": "並行模式", - "parallelModeDesc": "同時掃描多個錄製片段(速度更快,但 CPU 佔用會顯著升高)", + "parallelModeDesc": "同時掃描多個錄影片段(速度更快,但會消耗更多解碼資源)", "threshold": "靈敏度閾值", "thresholdDesc": "數值越低,可偵測到越小的變化(取值範圍 1-255)", "minArea": "最小變化區域", @@ -68,6 +70,9 @@ "framesDecoded": "畫面已解碼", "wallTime": "搜尋時間", "segmentErrors": "片段異常", - "seconds": "{{seconds}} 秒" - } + "seconds": "{{seconds}} 秒", + "minutesSeconds": "{{minutes}}分 {{seconds}}秒", + "scanSummary": "{{segments}} 個片段 · {{time}}" + }, + "scanning": "掃描中 {{time}}" } diff --git a/web/public/locales/zh-Hant/views/settings.json b/web/public/locales/zh-Hant/views/settings.json index 5252467276..2d25590493 100644 --- a/web/public/locales/zh-Hant/views/settings.json +++ b/web/public/locales/zh-Hant/views/settings.json @@ -53,7 +53,7 @@ "systemAuthentication": "驗證", "systemNetworking": "網路", "systemProxy": "代理", - "systemUi": "介面", + "systemUi": "使用者介面", "systemLogging": "日誌", "systemEnvironmentVariables": "環境變數", "systemTelemetry": "遙測", @@ -167,12 +167,12 @@ "label": "模型大小", "small": { "title": "小", - "desc": "將使用 模型。該模型使用的記憶體較少,在 CPU 上也能較快的執行,品質較好。" + "desc": "使用 small 將採用量化版本的模型。該模型佔用的記憶體(RAM)較少,在 CPU 上的執行速度也較快,且在嵌入向量品質上的差異微乎其微。" }, "desc": "用於語意搜尋的語言模型大小。", "large": { "title": "大", - "desc": "將使用 模型。該選項使用了完整的 Jina 模型,條件允許的情況下將自動使用 GPU 執行。" + "desc": "使用 large 將採用完整的 Jina 模型,並在支援的情況下自動於 GPU 上執行。" } }, "title": "語意搜尋", @@ -196,11 +196,11 @@ "desc": "用於人臉辨識的模型大小。", "small": { "title": "小", - "desc": "將使用模型。該選項採用 FaceNet 人臉特徵提取模型,可在大多數 CPU 上高效執行。" + "desc": "使用 模型將採用 FaceNet 臉部嵌入模型,可在大多數 CPU 上高效執行。" }, "large": { "title": "大", - "desc": "將使用模型。該選項使用 ArcFace 人臉特徵提取模型,條件允許的情況下將自動使用 GPU 執行。" + "desc": "使用 模型將採用 ArcFace 臉部嵌入模型,並在適用情況下自動於 GPU 上執行。" } } }, @@ -246,7 +246,7 @@ "customUrl": "自訂影片流地址", "brandInformation": "品牌資訊", "brandUrlFormat": "對於採用RTSP URL格式的攝影機,其格式為:{{exampleUrl}}", - "customUrlPlaceholder": "rtsp://使用者名稱:密碼@主機或IP地址:埠/路徑", + "customUrlPlaceholder": "rtsp://使用者名稱:密碼@主機或ip位址:通訊埠/路徑", "connectionSettings": "連線設定", "detectionMethod": "影片流偵測方法", "onvifPort": "ONVIF 埠", @@ -597,6 +597,32 @@ "title": "修改顯示名稱", "description": "設定該攝像機在 Frigate 使用者介面中顯示的名稱。若留空,則使用攝像機 ID。", "rename": "重新命名" + }, + "label": "攝影機狀態", + "description": "設定各攝影機的運作狀態。

開啟:正常處理串流。
關閉:暫時暫停處理。重啟 Frigate 後不會保留此狀態。
停用:停止處理並將變更儲存至您的設定中。需要重新啟動才能重新啟用已停用的攝影機。

備註:停用不會影響 go2rtc 的轉流功能。

拖曳控制鍵可調整現用攝影機在整個 UI 中顯示的順序,包括即時儀表板和攝影機選擇下拉選單。", + "disabledSubheading": "在設定中禁用", + "status": { + "on": "開啟", + "off": "關閉", + "disabled": "已停用" + }, + "disableSuccess": "已停用 {{cameraName}} 並儲存至設定。", + "reorderHandle": "拖曳以重新排序", + "saving": "儲存中…", + "saved": "已儲存", + "details": { + "edit": "編輯攝影機詳細資料", + "title": "編輯攝影機詳細資料", + "description": "更新此攝影機在整個 Frigate UI 中使用的顯示名稱、外部 URL 和可見性。", + "friendlyNameLabel": "顯示名稱", + "friendlyNameHelp": "在整個 Frigate UI 中顯示此攝影機的顯示名稱。留空則會使用攝影機 ID。", + "webuiUrlLabel": "攝影機網頁介面網址", + "webuiUrlHelp": "可從「偵錯」檢視中直接造訪攝影機網頁介面的網址。留空以停用此連結。", + "webuiUrlInvalid": "必須是有效的網址(例如:https://example.com)。", + "dashboardLabel": "在即時儀表板上顯示", + "dashboardHelp": "在即時儀表板上顯示此攝影機。", + "reviewLabel": "在審查中顯示", + "reviewHelp": "在「審查」中顯示此攝影機,包括攝影機篩選器、移動審查以及歷史檢視。" } }, "cameraConfig": { @@ -634,7 +660,9 @@ "description": "配置在啟用某個設定檔時,哪些攝影機應被開啟或關閉。設定為“繼承”的攝影機會沿用它原本的啟用/停用狀態。", "inherit": "繼承", "enabled": "開啟", - "disabled": "關閉" + "disabled": "關閉", + "on": "開啟", + "off": "關閉" }, "cameraType": { "title": "攝影機型別", @@ -643,6 +671,88 @@ "normal": "通用", "dedicatedLpr": "車牌辨識專用", "saveSuccess": "已更新 {{cameraName}} 的攝影機型別,請重啟 Frigate 以使更改生效。" + }, + "clone": { + "sectionTitle": "複製設定", + "sectionDescription": "將攝影機的設定複製到另一台攝影機或新攝影機。", + "button": "複製設定", + "title": "複製攝影機設定", + "description": "將攝影機的設定複製到一或多台其他攝影機,或是新攝影機。識別資料(名稱、顯示名稱、網頁介面網址、顯示順序)將不會被複製。", + "source": { + "label": "來源攝影機", + "placeholder": "選擇一個來源攝影機", + "required": "選擇一個來源攝影機" + }, + "target": { + "legend": "目標", + "newRadio": "新攝影機", + "newNameLabel": "攝影機名稱", + "newNamePlaceholder": "例如:back_door 或 Back Door", + "newNameRequired": "必須填寫攝影機名稱", + "newNameInvalid": "無效的攝影機名稱", + "newNameCollision": "已存在相同名稱的攝影機", + "newStreamsForced": "建立新攝影機時,一律會複製串流設定。", + "existingCamerasRadio": "現有攝影機", + "allCameras": "所有攝影機", + "existingPlaceholder": "請至少選擇一台攝影機", + "existingDisabled": "沒有其他攝影機可供複製" + }, + "categories": { + "legend": "要複製的設定", + "description": "選擇要從來源攝影機複製哪些設定。", + "selectAll": "全選", + "selectNone": "取消全選", + "resetDefaults": "重設為預設值", + "general": "一般", + "spatial": "空間設定", + "streams": "串流", + "spatialWarningTitle": "解析度不符", + "spatialWarning": "來源攝影機 {{srcCamera}} 的偵測解析度 ({{srcWidth}}×{{srcHeight}}) 與以下攝影機不符:{{cameras}}。這些攝影機上的多邊形(偵測區域)可能無法對齊。此為預設關閉;啟用即可按原樣複製。", + "restartHint": "需要重新啟動", + "items": { + "record": "錄影", + "snapshots": "快照", + "review": "審核", + "motion": "移動偵測", + "objects": "物件", + "audio": "聲音偵測", + "audio_transcription": "語音轉文字", + "notifications": "通知", + "birdseye": "鳥瞰圖", + "mqtt": "MQTT", + "timestamp_style": "時間戳記樣式", + "onvif": "ONVIF", + "lpr": "車牌辨識", + "face_recognition": "人臉辨識", + "semantic_search": "語意搜尋", + "genai": "生成式 AI", + "type": "攝影機類型(一般 / 車牌識別專用)", + "profiles": "設定檔", + "detect": "偵測解析度", + "zones": "區域", + "motion_mask": "移動偵測遮罩", + "object_masks": "物件遮罩", + "ffmpeg_live": "串流網址與用途" + } + }, + "footer": { + "changeCount_other": "將套用 {{count}} 項變更", + "restartNeeded": "部分變更需要重新啟動。", + "liveOnly": "所有變更將即時生效,無需重新啟動。", + "submit": "複製", + "submitting": "複製中…" + }, + "toast": { + "success": "設定已複製到 {{cameraName}}", + "successWithRestart": "設定已複製到 {{cameraName}}。請重新啟動 Frigate 以套用所有變更。", + "successMulti_other": "設定已複製到 {{count}} 台攝影機", + "successMultiWithRestart_other": "設定已複製到 {{count}} 台攝影機。請重新啟動 Frigate 以套用所有變更。", + "partialFailure": "已套用 {{successCount}} 個區段;「{{failedSection}}」失敗:{{errorMessage}}", + "partialFailureMulti": "設定已複製到 {{successCount}} 台攝影機;{{failed}} 失敗:{{errorMessage}}", + "newCameraPartialFailure": "攝影機 {{cameraName}} 已建立,但部分設定複製失敗:{{errorMessage}}", + "sourceMissing": "來源攝影機已不存在", + "submitError": "無法複製攝影機:{{errorMessage}}" + } } }, "cameraReview": { @@ -663,7 +773,7 @@ }, "reviewClassification": { "title": "審閱分類", - "desc": "Frigate 將審閱項的嚴重程度分為“警報”和“偵測”兩個等級。預設情況下,所有的汽車 目標都將視為警報。你可以透過修改設定檔配置區域來細分。", + "desc": "Frigate 將審查項目分類為警報與偵測。預設情況下,所有 汽車 物件都會被視為警報。您可以透過為其設定所需的區域,來進一步精確分類您的審查項目。", "noDefinedZones": "此攝影機未設定任何監控區。", "objectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下都將視為警報。", "zoneObjectAlertsTips": "所有 {{alertsLabels}} 類目標或物體在 {{cameraName}} 下的 {{zone}} 區域內都將視為警報。", @@ -760,7 +870,7 @@ }, "delete": { "title": "確認刪除", - "desc": "你確定要刪除{{type}} “{{name}}” 嗎?", + "desc": "你確定要刪除 {{type}} {{name}} 嗎?", "success": "{{name}} 已被刪除。" }, "revertOverride": { @@ -838,7 +948,7 @@ "edit": "編輯畫面變動遮罩", "defaultName": "畫面變動遮罩 {{number}}", "context": { - "title": "畫面變動遮罩用於防止不需要的畫面變動觸發偵測(例如:容易被風吹動的樹枝、攝影機畫面上顯示的時間等)。畫面變動遮罩應謹慎使用,過度的遮罩會導致追蹤目標變得更加困難。" + "title": "移動遮罩用於防止不需要的移動觸發偵測(例如:樹枝搖晃、攝影機時間戳記等)。移動遮罩應 非常謹慎地使用,過度的遮罩會導致物件追蹤變得更加困難。" }, "point_other": "{{count}} 點", "clickDrawPolygon": "在影像上點選新增點繪製多邊形區域。", @@ -937,7 +1047,7 @@ "desc": "將在被追蹤的目標周圍顯示邊界框", "colors": { "label": "目標邊界框顏色定義", - "info": "
  • 啟用後,將會為每個目標的標籤分配不同的顏色
  • 深藍色細線代表該目標或物體在當前時間點未被偵測到
  • 灰色細線代表偵測到的目標或物體靜止不動
  • 粗線表示在啟動自動追蹤時,該目標為自動追蹤的主體
  • " + "info": "
  • 啟動時,系統會為每個物件標籤指派不同的顏色
  • 深藍色細線代表該物件目前未被偵測到
  • 灰色細線代表該物件被偵測為靜止狀態
  • 粗線代表該物件正被自動追蹤(若已啟用)
  • " } }, "timestamp": { @@ -1163,7 +1273,8 @@ }, "notificationUnavailable": { "title": "通知功能不可用", - "desc": "網頁推送通知需要安全連線(https://…)。這是瀏覽器的限制。請透過安全方式存取 Frigate 以使用通知功能。" + "desc": "網頁推送通知需要安全連線(https://…)。這是瀏覽器的限制。請透過安全方式存取 Frigate 以使用通知功能。", + "descPwa": "在 iOS 上,只有將 Frigate 加到主畫面後,才能使用網頁推播通知。請開啟 分享 選單,選擇 加入主畫面,然後從新圖示開啟 Frigate,即可為此裝置註冊通知功能。" }, "globalSettings": { "title": "全域性設定", @@ -1425,7 +1536,17 @@ } }, "cameraInputs": { - "itemTitle": "影片流 {{index}}" + "itemTitle": "影片流 {{index}}", + "sourceMode": { + "restream": "重新串流 (go2rtc)", + "manual": "手動輸入路徑", + "go2rtcStreamLabel": "go2rtc 串流", + "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流", + "noGo2rtcStreams": "未設定 go2rtc 串流", + "go2rtcStreamSearch": "搜尋串流...", + "availableStreams": "可用串流", + "noMatchingStreams": "沒有符合的串流" + } }, "restartRequiredField": "需要重啟", "restartRequiredFooter": "配置已更改 - 需要重啟", @@ -1542,7 +1663,37 @@ "genaiModel": { "placeholder": "選擇模型…", "search": "搜尋模型…", - "noModels": "暫無模型" + "noModels": "暫無模型", + "available": "可用模型", + "useCustom": "使用 \"{{value}}\"", + "refresh": "重新整理模型清單", + "probeFailed": "無法取得模型清單", + "fetchedModels": "已成功取得模型清單" + }, + "liveStreams": { + "streamNameLabel": "串流名稱", + "streamNamePlaceholder": "例如:主高畫質串流", + "go2rtcStreamLabel": "go2rtc 串流", + "go2rtcStreamPlaceholder": "選擇一個 go2rtc 串流", + "go2rtcStreamSearch": "搜尋或輸入串流名稱…", + "noGo2rtcStreams": "未設定 go2rtc 串流", + "availableStreams": "可用串流", + "useCustom": "使用 \"{{value}}\"", + "addStream": "新增串流" + }, + "semanticSearchModelSize": { + "notApplicable": "不適用於生成式 AI 服務" + }, + "ptzPresets": { + "placeholder": "選擇或輸入預設...", + "search": "選擇或輸入預設...", + "noPresets": "沒有可用的預設", + "available": "攝影機預設", + "useCustom": "使用 \"{{value}}\"" + }, + "defaultRole": { + "admin": "管理員", + "viewer": "檢視者" } }, "globalConfig": { @@ -1574,7 +1725,8 @@ "resetError": "重設設定失敗", "saveAllSuccess_other": "所有 {{count}} 個部分儲存成功。", "saveAllPartial_other": "已儲存 {{successCount}} / {{totalCount}} 個部分。{{failCount}} 個失敗。", - "saveAllFailure": "儲存所有部分失敗。" + "saveAllFailure": "儲存所有部分失敗。", + "saveAllSuccessRestartRequired_other": "已成功儲存 {{count}} 個區塊。請重新啟動 Frigate 以套用變更。" }, "profiles": { "title": "設定檔", @@ -1660,14 +1812,30 @@ "audioMp3": "轉碼為 MP3", "audioExclude": "排除", "hardwareNone": "無硬體加速", - "hardwareAuto": "自動選擇硬體加速" - } + "hardwareAuto": "自動選擇硬體加速", + "hardwareVaapi": "VAAPI", + "hardwareCuda": "CUDA", + "hardwareV4l2m2m": "V4L2 M2M", + "hardwareDxva2": "DXVA2", + "hardwareVideotoolbox": "VideoToolbox", + "addVideoCodec": "新增視訊編碼器", + "addAudioCodec": "新增音訊編碼器", + "removeCodec": "移除編碼器" + }, + "sourceNumber": "來源 {{index}}" }, "birdseye": { "trackingMode": { "objects": "目標", "motion": "動作", "continuous": "持續" + }, + "cameraOrder": { + "label": "攝影機排序", + "description": "拖曳攝影機以設定它們在鳥瞰佈局中的順序。", + "reorderHandle": "拖曳以重新排序", + "saving": "儲存中…", + "saved": "已儲存" } }, "retainMode": { @@ -1709,9 +1877,9 @@ "logLevel": { "debug": "Debug", "info": "Info", - "warning": "Warning", + "warning": "警告", "error": "Error", - "critical": "Critical" + "critical": "嚴重" } }, "onvif": { @@ -1744,7 +1912,13 @@ }, "detect": { "fpsGreaterThanFive": "不建議設定偵測幀率高於 5,數值設定過高可能引發效能問題,且不會帶來任何增益。", - "disabled": "目標偵測已停用。快照、回放條目以及人臉辨識、車牌辨識、生成式 AI 等增強功能都將無法使用。" + "disabled": "目標偵測已停用。快照、回放條目以及人臉辨識、車牌辨識、生成式 AI 等增強功能都將無法使用。", + "resolutionShouldBeMultipleOfFour": "為了獲得最佳效果,偵測的寬度與高度應為 4 的倍數。其他偶數值可能會在偵測串流中產生畫面殘影或輕微失真。", + "aspectRatioMismatch": "您輸入的寬度與高度與目前的偵測解析度長寬比不符。這可能會導致影像被拉伸或失真。", + "maxFramesSet": "設定最大影格數將會覆蓋預設行為,並停用靜態物件追蹤。極少有情況需要使用此功能,請謹慎使用。", + "squareResolution": "正方形的偵測解析度並不常見。偵測的寬度與高度應符合攝影機的長寬比(例如 16:9),而不是物件偵測模型的尺寸。不相符的長寬比可能會拉伸影像並降低偵測準確率度。", + "resolutionHigh": "此偵測解析度高於建議值,可能會增加資源消耗,但不會提高偵測準確度。對於大多數攝影機,建議使用 1080p 或更低的偵測解析度。", + "globalResolutionMultipleCameras": "在設定多台攝影機的情況下設定了全域偵測解析度。除非所有攝影機都具有相同的解析度和長寬比,否則應針對每台攝影機單獨定義偵測寬度與高度,以符合各台攝影機的原生長寬比。" }, "objects": { "genaiNoDescriptionsProvider": "必須配置具備“描述”功能的生成式 AI 服務商,才能自動生成事件描述。" @@ -1774,6 +1948,21 @@ }, "semanticSearch": { "jinav2SmallModelSize": "Jina V2 的大型模型版本記憶體佔用與推理開銷較高,建議搭配獨立顯示卡使用大型模型。" + }, + "model": { + "optimizedFor320": "Frigate 已針對 320x320 模型進行最佳化,這是大多數環境下的最佳選擇。640x640 模型速度較慢,且僅在特定情境下有所幫助。", + "inputDimensionsNotDetectResolution": "模型輸入的寬度與高度是物件偵測模型的輸入尺寸,而不是攝影機的偵測解析度。它們應該與您使用的模型尺寸相符 — 通常是像 320x320 或 640x640 這樣的正方形尺寸。" + }, + "ffmpeg": { + "hwaccelManualNotRecommended": "不建議手動設定硬體加速參數。除非有特殊需求,否則請選擇符合您硬體的預設組態。" + }, + "onvif": { + "autotrackingNoZones": "自動追蹤至少需要一個區域。請先在「遮罩 / 區域」中為此攝影機定義一個區域,然後在下方將其設定為必要區域。" } + }, + "menuDot": { + "overrideGlobal": "此區段會覆蓋全域設定", + "overrideProfile": "此區段已被 {{profile}} 設定檔覆蓋", + "unsaved": "此區段有未儲存的變更" } } diff --git a/web/public/locales/zh-Hant/views/system.json b/web/public/locales/zh-Hant/views/system.json index 23aa19f880..df8c0a7ca1 100644 --- a/web/public/locales/zh-Hant/views/system.json +++ b/web/public/locales/zh-Hant/views/system.json @@ -162,7 +162,7 @@ "info": { "aspectRatio": "長寬比", "cameraProbeInfo": "{{camera}} 的詳細資訊", - "streamDataFromFFPROBE": "串流資料是透過 ffprobe取得。", + "streamDataFromFFPROBE": "串流資料是透過 ffprobe 取得。", "fetching": "正在讀取鏡頭資訊", "stream": "串流 {{idx}}", "video": "影片:", @@ -174,6 +174,21 @@ "error": "錯誤:{{error}}", "tips": { "title": "鏡頭詳細資訊" + }, + "keyframes": { + "title": "關鍵幀分析", + "analyzing": "正在分析關鍵幀... 剩餘 {{seconds}} 秒", + "stillAnalyzing": "仍在分析關鍵幀...", + "recordStream": "錄影串流:", + "keyframeCount": "偵測到的關鍵幀:", + "observedDuration": "觀測持續時間:", + "gap": "關鍵幀間隔 (最小 / 平均 / 最大):", + "segmentLength": "錄影片段長度:", + "ok": "每約 {{seconds}} 秒一個關鍵幀,適合錄影與回放。", + "warning": "關鍵幀稀疏或不固定(最長間隔約 {{seconds}} 秒),可能使用了智慧編碼(H.264+/H.265+),不建議這樣設定。", + "error": "關鍵幀間隔(~{{seconds}} 秒)超過了錄影片段長度({{segmentTime}} 秒)。某些片段可能沒有關鍵幀,這會導致回放中斷。請停用攝影機上的智慧/+編碼,或縮短其關鍵幀間隔。", + "unknown": "無法確定關鍵幀間隔。", + "recordDisabled": "此攝影機已停用錄影功能。" } }, "framesAndDetections": "幀數 / 偵測數", diff --git a/web/src/api/WsProvider.tsx b/web/src/api/WsProvider.tsx index d73e5a3090..eb0d76397f 100644 --- a/web/src/api/WsProvider.tsx +++ b/web/src/api/WsProvider.tsx @@ -2,7 +2,11 @@ import { baseUrl } from "./baseUrl"; import { ReactNode, useCallback, useEffect, useRef } from "react"; import { WsSendContext } from "./wsContext"; import type { Update } from "./wsContext"; -import { processWsMessage, resetWsStore } from "./ws"; +import { + invalidateCameraActivityCache, + processWsMessage, + resetWsStore, +} from "./ws"; export function WsProvider({ children }: { children: ReactNode }) { const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`; @@ -34,6 +38,9 @@ export function WsProvider({ children }: { children: ReactNode }) { ws.onopen = () => { reconnectAttempt.current = 0; + // events may have been missed while disconnected — the snapshot + // requested below must fully apply even if byte-identical + invalidateCameraActivityCache(); ws.send( JSON.stringify({ topic: "onConnect", message: "", retain: false }), ); diff --git a/web/src/api/ws.ts b/web/src/api/ws.ts index 909a1bb5d9..0a91a88da7 100644 --- a/web/src/api/ws.ts +++ b/web/src/api/ws.ts @@ -82,12 +82,15 @@ export function processWsMessage(raw: string) { function applyTopicUpdate(topic: string, newVal: unknown) { const oldVal = wsState[topic]; + // camera_activity snapshots always re-notify: consumers reconcile local + // state that may have diverged from an unchanged snapshot + const isActivitySnapshot = topic.startsWith("camera_activity/"); // Fast path: === for primitives ("ON"/"OFF", numbers). // Fall back to isEqual for objects/arrays. const unchanged = oldVal === newVal || (typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal)); - if (unchanged) return; + if (unchanged && !isActivitySnapshot) return; wsState[topic] = newVal; // Snapshot the Set — a listener may trigger unmount that modifies it. @@ -131,6 +134,25 @@ let wsMessageIdCounter = 0; // traversals) on every flush — critical with many cameras. let lastCameraActivityPayload: string | null = null; +// Make the next camera_activity snapshot fully apply even when byte-identical +// to the previous one — local state may have diverged while no messages flowed +export function invalidateCameraActivityCache() { + lastCameraActivityPayload = null; +} + +// Collapse same-task resync requests (one hook per camera card) into a +// single onConnect round-trip +let resyncScheduled = false; +function requestCameraActivityResync(sendOnConnect: () => void) { + if (resyncScheduled) return; + resyncScheduled = true; + queueMicrotask(() => { + resyncScheduled = false; + invalidateCameraActivityCache(); + sendOnConnect(); + }); +} + function applyCameraActivity(payload: string) { // Fast path: if the raw JSON string is identical, nothing changed. if (payload === lastCameraActivityPayload) return; @@ -509,15 +531,16 @@ export function useInitialCameraState( // camera_activity sub-topic payload is already parsed by expandCameraActivity const data = payload as FrigateCameraState | undefined; - // onConnect is sent once in WsProvider.onopen — no need to re-request on - // every component mount. Components read cached wsState immediately via - // useSyncExternalStore. Only re-request when the user tabs back in. + // the cached snapshot is only written on onConnect and can be stale by the + // time this hook mounts — re-request on mount and when the user tabs back in useEffect(() => { if (!revalidateOnFocus) return; + requestCameraActivityResync(() => sendCommand("onConnect")); + const listener = () => { if (document.visibilityState === "visible") { - sendCommand("onConnect"); + requestCameraActivityResync(() => sendCommand("onConnect")); } }; addEventListener("visibilitychange", listener); diff --git a/web/src/components/classification/ClassificationModelEditDialog.tsx b/web/src/components/classification/ClassificationModelEditDialog.tsx index ac49ee4db7..4be2e04f95 100644 --- a/web/src/components/classification/ClassificationModelEditDialog.tsx +++ b/web/src/components/classification/ClassificationModelEditDialog.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -17,6 +18,7 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -50,14 +52,25 @@ type ClassificationModelEditDialogProps = { type ObjectClassificationType = "sub_label" | "attribute"; type ObjectFormData = { + enabled: boolean; + saveAttempts: number; objectLabel: string; objectType: ObjectClassificationType; }; type StateFormData = { + enabled: boolean; + saveAttempts: number; + motion: boolean; + interval?: number; classes: string[]; }; +const DEFAULT_SAVE_ATTEMPTS = { + object: 200, + state: 100, +} as const; + export default function ClassificationModelEditDialog({ open, model, @@ -71,6 +84,10 @@ export default function ClassificationModelEditDialog({ const isStateModel = model.state_config !== undefined; const isObjectModel = model.object_config !== undefined; + const defaultSaveAttempts = isObjectModel + ? DEFAULT_SAVE_ATTEMPTS.object + : DEFAULT_SAVE_ATTEMPTS.state; + const objectLabels = useMemo(() => { if (!config) return []; @@ -93,8 +110,17 @@ export default function ClassificationModelEditDialog({ // Define form schema based on model type const formSchema = useMemo(() => { + const sharedFields = { + enabled: z.boolean(), + saveAttempts: z.coerce + .number({ message: t("edit.errors.saveAttemptsInvalid") }) + .int(t("edit.errors.saveAttemptsInvalid")) + .min(0, t("edit.errors.saveAttemptsInvalid")), + }; + if (isObjectModel) { return z.object({ + ...sharedFields, objectLabel: z .string() .min(1, t("wizard.step1.errors.objectLabelRequired")), @@ -103,6 +129,17 @@ export default function ClassificationModelEditDialog({ } else { // State model return z.object({ + ...sharedFields, + motion: z.boolean(), + interval: z.preprocess( + (val) => + val === "" || val === null || val === undefined ? undefined : val, + z.coerce + .number({ message: t("edit.errors.intervalInvalid") }) + .int(t("edit.errors.intervalInvalid")) + .positive(t("edit.errors.intervalInvalid")) + .optional(), + ), classes: z .array(z.string()) .min(1, t("wizard.step1.errors.classRequired")) @@ -129,12 +166,18 @@ export default function ClassificationModelEditDialog({ resolver: zodResolver(formSchema), defaultValues: isObjectModel ? ({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, objectLabel: model.object_config?.objects?.[0] || "", objectType: (model.object_config ?.classification_type as ObjectClassificationType) || "sub_label", } as ObjectFormData) : ({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, + motion: model.state_config?.motion ?? false, + interval: model.state_config?.interval, classes: [""], // Will be populated from dataset } as StateFormData), mode: "onChange", @@ -151,6 +194,8 @@ export default function ClassificationModelEditDialog({ if (open) { if (isObjectModel) { form.reset({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, objectLabel: model.object_config?.objects?.[0] || "", objectType: (model.object_config @@ -158,6 +203,10 @@ export default function ClassificationModelEditDialog({ } as ObjectFormData); } else { form.reset({ + enabled: model.enabled, + saveAttempts: model.save_attempts ?? defaultSaveAttempts, + motion: model.state_config?.motion ?? false, + interval: model.state_config?.interval, classes: [""], } as StateFormData); } @@ -166,7 +215,15 @@ export default function ClassificationModelEditDialog({ mutateDataset(); } } - }, [open, isObjectModel, isStateModel, model, form, mutateDataset]); + }, [ + open, + isObjectModel, + isStateModel, + model, + form, + mutateDataset, + defaultSaveAttempts, + ]); // Update form with classes from dataset when loaded useEffect(() => { @@ -233,6 +290,7 @@ export default function ClassificationModelEditDialog({ setIsSaving(true); try { if (isObjectModel) { + // object model save const objectData = data as ObjectFormData; // Update the config @@ -243,9 +301,10 @@ export default function ClassificationModelEditDialog({ classification: { custom: { [model.name]: { - enabled: model.enabled, + enabled: objectData.enabled, name: model.name, threshold: model.threshold, + save_attempts: objectData.saveAttempts, object_config: { objects: [objectData.objectLabel], classification_type: objectData.objectType, @@ -260,7 +319,34 @@ export default function ClassificationModelEditDialog({ position: "top-center", }); } else { + // state model save const stateData = data as StateFormData; + + const stateConfig: { motion: boolean; interval?: number | null } = { + motion: stateData.motion, + }; + if (stateData.interval != null) { + stateConfig.interval = stateData.interval; + } else if (model.state_config?.interval != null) { + stateConfig.interval = null; + } + + await axios.put("/config/set", { + requires_restart: 0, + update_topic: `config/classification/custom/${model.name}`, + config_data: { + classification: { + custom: { + [model.name]: { + enabled: stateData.enabled, + save_attempts: stateData.saveAttempts, + state_config: stateConfig, + }, + }, + }, + }, + }); + const newClasses = stateData.classes.filter( (c) => c.trim().length > 0, ); @@ -307,11 +393,11 @@ export default function ClassificationModelEditDialog({ if (renamePromises.length > 0) { await Promise.all(renamePromises); await mutate(`classification/${model.name}/dataset`); - toast.success(t("toast.success.updatedModel"), { + toast.success(t("edit.stateClassesInfo"), { position: "top-center", }); } else { - toast.info(t("edit.stateClassesInfo"), { + toast.success(t("toast.success.updatedModel"), { position: "top-center", }); } @@ -359,6 +445,29 @@ export default function ClassificationModelEditDialog({
    + ( + +
    + + {t("edit.enabled")} + + + {t("edit.enabledDesc")} + +
    + + + +
    + )} + /> + {isObjectModel && ( <> )} + {isStateModel && ( + <> + ( + +
    + + {t("edit.motion")} + + + {t("edit.motionDesc")} + +
    + + + +
    + )} + /> + + ( + + + {t("edit.interval")} + + + + + + {t("edit.intervalDesc")} + + + + )} + /> + + )} + + ( + + + {t("edit.saveAttempts")} + + + + + + {t("edit.saveAttemptsDesc")} + + + + )} + /> +
    diff --git a/web/src/components/config-form/theme/fields/CameraInputsField.tsx b/web/src/components/config-form/theme/fields/CameraInputsField.tsx index ee19dbc95d..205e888c9e 100644 --- a/web/src/components/config-form/theme/fields/CameraInputsField.tsx +++ b/web/src/components/config-form/theme/fields/CameraInputsField.tsx @@ -29,11 +29,19 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { StreamSourceSelector } from "./StreamSourceSelector"; +import { + buildRestreamPath, + parseRestreamStreamName, + RESTREAM_PRESET, + type StreamSourceMode, +} from "./streamSource"; type FfmpegInput = { path?: string; roles?: string[]; hwaccel_args?: unknown; + input_args?: unknown; }; const asInputList = (formData: unknown): FfmpegInput[] => { @@ -137,7 +145,30 @@ export function CameraInputsField(props: FieldProps) { ); const SchemaField = registry.fields.SchemaField; + const go2rtcStreamNames = useMemo(() => { + const streams = formContext?.fullConfig?.go2rtc?.streams; + if (!streams || typeof streams !== "object") { + return []; + } + return Object.keys(streams).sort(); + }, [formContext?.fullConfig?.go2rtc?.streams]); + const [openByIndex, setOpenByIndex] = useState>({}); + const [sourceModeByIndex, setSourceModeByIndex] = useState< + Record + >({}); + + // Detect whether an existing input path points at a known go2rtc restream so + // the source toggle can default to the right mode for existing configs. + const detectMode = useCallback( + (path: string | undefined): StreamSourceMode => { + const streamName = parseRestreamStreamName(path); + return streamName && go2rtcStreamNames.includes(streamName) + ? "restream" + : "manual"; + }, + [go2rtcStreamNames], + ); useEffect(() => { setOpenByIndex((previous) => { @@ -171,6 +202,55 @@ export function CameraInputsField(props: FieldProps) { [fieldPathId.path, inputs, onChange], ); + // Update several fields of one input in a single change so that path and + // input_args never race on a stale snapshot of inputs. + const handleFieldValuesChange = useCallback( + (index: number, partial: Record) => { + const nextInputs = cloneDeep(inputs); + const item = + (nextInputs[index] as Record | undefined) ?? + ({} as Record); + + Object.assign(item, partial); + nextInputs[index] = item; + + onChange(normalizeNonDetectHwaccel(nextInputs), fieldPathId.path); + }, + [fieldPathId.path, inputs, onChange], + ); + + const handleSourceModeChange = useCallback( + (index: number, nextMode: StreamSourceMode) => { + const input = inputs[index]; + const currentPath = + typeof input?.path === "string" ? input.path : undefined; + + if (nextMode === "manual") { + // Only revert the preset we set ourselves; never clobber custom args. + if (input?.input_args === RESTREAM_PRESET) { + handleFieldValuesChange(index, { input_args: undefined }); + } + } else if (!parseRestreamStreamName(currentPath)) { + // Entering restream with a non-restream path: clear it so the dropdown + // shows its placeholder until a stream is chosen. + handleFieldValuesChange(index, { path: undefined }); + } + + setSourceModeByIndex((previous) => ({ ...previous, [index]: nextMode })); + }, + [inputs, handleFieldValuesChange], + ); + + const handleSelectRestreamStream = useCallback( + (index: number, streamName: string) => { + handleFieldValuesChange(index, { + path: buildRestreamPath(streamName), + input_args: RESTREAM_PRESET, + }); + }, + [handleFieldValuesChange], + ); + const handleAddInput = useCallback(() => { const base = itemSchema ? (applySchemaDefaults(itemSchema) as FfmpegInput) @@ -186,8 +266,9 @@ export function CameraInputsField(props: FieldProps) { (_, currentIndex) => currentIndex !== index, ); onChange(nextInputs, fieldPathId.path); - setOpenByIndex((previous) => { - const next: Record = {}; + + const reindex = (previous: Record): Record => { + const next: Record = {}; Object.entries(previous).forEach(([key, value]) => { const current = Number(key); if (Number.isNaN(current) || current === index) { @@ -197,7 +278,10 @@ export function CameraInputsField(props: FieldProps) { next[current > index ? current - 1 : current] = value; }); return next; - }); + }; + + setOpenByIndex(reindex); + setSourceModeByIndex(reindex); }, [fieldPathId.path, inputs, onChange], ); @@ -354,16 +438,32 @@ export function CameraInputsField(props: FieldProps) {
    - {renderField(index, "path", { - extraUiSchema: { - "ui:widget": "CameraPathWidget", - "ui:options": { - size: "full", - splitLayout: false, + + handleSourceModeChange(index, nextMode) + } + streamNames={go2rtcStreamNames} + selectedStreamName={ + parseRestreamStreamName(input.path) ?? "" + } + onSelectStream={(streamName) => + handleSelectRestreamStream(index, streamName) + } + manualField={renderField(index, "path", { + extraUiSchema: { + "ui:widget": "CameraPathWidget", + "ui:options": { + size: "full", + splitLayout: false, + }, }, - }, - showSchemaDescription: true, - })} + showSchemaDescription: true, + })} + disabled={disabled} + readonly={readonly} + />
    {renderField(index, "roles")}
    diff --git a/web/src/components/config-form/theme/fields/StreamSourceSelector.tsx b/web/src/components/config-form/theme/fields/StreamSourceSelector.tsx new file mode 100644 index 0000000000..4bcfeac58c --- /dev/null +++ b/web/src/components/config-form/theme/fields/StreamSourceSelector.tsx @@ -0,0 +1,217 @@ +import type { ReactNode } from "react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; +import { Check, ChevronsUpDown } from "lucide-react"; +import type { StreamSourceMode } from "./streamSource"; + +type Go2rtcStreamComboboxProps = { + id: string; + value: string; + options: string[]; + disabled?: boolean; + onSelect: (streamName: string) => void; +}; + +// Searchable dropdown of existing go2rtc streams +function Go2rtcStreamCombobox({ + id, + value, + options, + disabled, + onSelect, +}: Go2rtcStreamComboboxProps) { + const { t } = useTranslation(["views/settings", "common"]); + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + const commit = (next: string) => { + onSelect(next); + setSearchValue(""); + setOpen(false); + }; + + return ( + { + setOpen(next); + if (!next) setSearchValue(""); + }} + > + + + + + + + + + {t("configForm.cameraInputs.sourceMode.noMatchingStreams")} + + + {options.map((option) => ( + commit(option)} + > + + {option} + + ))} + + + + + + ); +} + +type StreamSourceSelectorProps = { + idPrefix: string; + mode: StreamSourceMode; + onModeChange: (mode: StreamSourceMode) => void; + streamNames: string[]; + selectedStreamName: string; + onSelectStream: (streamName: string) => void; + manualField: ReactNode; + disabled?: boolean; + readonly?: boolean; +}; + +export function StreamSourceSelector({ + idPrefix, + mode, + onModeChange, + streamNames, + selectedStreamName, + onSelectStream, + manualField, + disabled, + readonly, +}: StreamSourceSelectorProps) { + const { t } = useTranslation(["views/settings", "common"]); + + const restreamId = `${idPrefix}-source-restream`; + const manualId = `${idPrefix}-source-manual`; + const selectId = `${idPrefix}-restream-select`; + + const hasStreams = streamNames.length > 0; + const isDisabled = disabled || readonly; + + return ( +
    + onModeChange(value as StreamSourceMode)} + className="flex flex-col gap-2 sm:flex-row sm:gap-6" + disabled={isDisabled} + > +
    + + +
    +
    + + +
    +
    + + {mode === "restream" ? ( +
    + + {hasStreams ? ( + + ) : ( +

    + {t("configForm.cameraInputs.sourceMode.noGo2rtcStreams")} +

    + )} +
    + ) : ( + manualField + )} +
    + ); +} + +export default StreamSourceSelector; diff --git a/web/src/components/config-form/theme/fields/streamSource.ts b/web/src/components/config-form/theme/fields/streamSource.ts new file mode 100644 index 0000000000..142f2b2ac9 --- /dev/null +++ b/web/src/components/config-form/theme/fields/streamSource.ts @@ -0,0 +1,33 @@ +export type StreamSourceMode = "restream" | "manual"; + +// The literal go2rtc restream prefix matches what the camera wizard inlines +// when it builds a restreamed input path. Only this exact host:port is treated +// as a restream so manually typed URLs (including localhost) stay manual. +export const RESTREAM_PREFIX = "rtsp://127.0.0.1:8554/"; +export const RESTREAM_PRESET = "preset-rtsp-restream"; + +/** Build the restream input path for a given go2rtc stream name. */ +export function buildRestreamPath(streamName: string): string { + return `${RESTREAM_PREFIX}${streamName}`; +} + +/** + * Extract the go2rtc stream name from a restream input path. + * + * Returns the stream name when the path is a well-formed restream URL with no + * extra path segments or query, otherwise undefined. + */ +export function parseRestreamStreamName( + path: string | undefined, +): string | undefined { + if (typeof path !== "string" || !path.startsWith(RESTREAM_PREFIX)) { + return undefined; + } + + const name = path.slice(RESTREAM_PREFIX.length); + if (name.length === 0 || /[/?#]/.test(name)) { + return undefined; + } + + return name; +} diff --git a/web/src/components/config-form/theme/templates/FieldTemplate.tsx b/web/src/components/config-form/theme/templates/FieldTemplate.tsx index 2ee91ae033..51817c4395 100644 --- a/web/src/components/config-form/theme/templates/FieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/FieldTemplate.tsx @@ -386,11 +386,14 @@ export function FieldTemplate(props: FieldTemplateProps) { const beforeContent = renderCustom(beforeSpec); const afterContent = renderCustom(afterSpec); - // Read field-level conditional messages from FieldMessagesContext + // Read field-level conditional messages from FieldMessagesContext. + // For multi-schema fields (anyOf/oneOf), FieldTemplate renders twice for + // the same path (wrapper + inner branch); skip the wrapper pass so the + // message isn't shown twice, mirroring how labels/descriptions dedupe. const fieldPathStr = pathSegments.join("."); - const fieldMessageSpecs = allFieldMessages.filter( - (m) => m.field === fieldPathStr, - ); + const fieldMessageSpecs = isMultiSchemaWrapper + ? [] + : allFieldMessages.filter((m) => m.field === fieldPathStr); const beforeMessages = fieldMessageSpecs.filter( (m) => (m.position ?? "before") === "before", ); @@ -403,6 +406,8 @@ export function FieldTemplate(props: FieldTemplateProps) { key={m.key} messageKey={m.messageKey} severity={m.severity} + values={m.values} + docLink={m.docLink} /> ))}
    @@ -415,6 +420,8 @@ export function FieldTemplate(props: FieldTemplateProps) { key={m.key} messageKey={m.messageKey} severity={m.severity} + values={m.values} + docLink={m.docLink} /> ))} diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index 9d703b3c8d..009a53b588 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -156,7 +156,8 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { }; const hasModifiedDescendants = checkSubtreeModified(fieldPath); - const [isOpen, setIsOpen] = useState(hasModifiedDescendants); + const defaultOpen = uiSchema?.["ui:options"]?.defaultOpen === true; + const [isOpen, setIsOpen] = useState(hasModifiedDescendants || defaultOpen); const resetKey = `${formContext?.level ?? "global"}::${ formContext?.cameraName ?? "global" }`; @@ -192,6 +193,8 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { (uiSchema?.["ui:groups"] as Record | undefined) || {}; const disableNestedCard = uiSchema?.["ui:options"]?.disableNestedCard === true; + const disableCollapsible = + uiSchema?.["ui:options"]?.disableCollapsible === true; const isHiddenProp = (prop: (typeof properties)[number]) => (prop.content.props as RjsfElementProps).uiSchema?.["ui:widget"] === @@ -228,10 +231,10 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { useEffect(() => { if (lastResetKeyRef.current !== resetKey) { lastResetKeyRef.current = resetKey; - setIsOpen(hasModifiedDescendants); + setIsOpen(hasModifiedDescendants || defaultOpen); setShowAdvanced(hasModifiedAdvanced); } - }, [resetKey, hasModifiedDescendants, hasModifiedAdvanced]); + }, [resetKey, hasModifiedDescendants, hasModifiedAdvanced, defaultOpen]); const { children } = props as ObjectFieldTemplateProps & { children?: ReactNode; }; @@ -458,6 +461,75 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { ); } + // Label/description/docs header shared by the collapsible and static layouts. + const cardHeaderContent = ( +
    + + {inferredLabel} + {objectRequiresRestart && } + + {inferredDescription && ( +

    + {inferredDescription} +

    + )} + {fieldDocsUrl && ( +
    + e.stopPropagation()} + > + {t("readTheDocumentation", { ns: "common" })} + + +
    + )} +
    + ); + + // Body shared by the collapsible and static layouts. + const cardBody = hasCustomChildren ? ( + children + ) : ( + <> + {renderGroupedFields(regularProps)} + + + + {renderGroupedFields(advancedProps)} + + + ); + + // Static (non-collapsible) card: keep the labeled header, always show content. + if (disableCollapsible) { + return ( + + {cardHeaderContent} + {cardBody} + + ); + } + // Nested objects render as collapsible cards return ( @@ -465,38 +537,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
    -
    - - {inferredLabel} - {objectRequiresRestart && ( - - )} - - {inferredDescription && ( -

    - {inferredDescription} -

    - )} - {fieldDocsUrl && ( -
    - e.stopPropagation()} - > - {t("readTheDocumentation", { ns: "common" })} - - -
    - )} -
    + {cardHeaderContent} {isOpen ? ( ) : ( @@ -506,31 +547,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { - - {hasCustomChildren ? ( - children - ) : ( - <> - {renderGroupedFields(regularProps)} - - - - {renderGroupedFields(advancedProps)} - - - )} - + {cardBody} diff --git a/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx b/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx index 6e6a19bfdd..9a769369ce 100644 --- a/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx +++ b/web/src/components/config-form/theme/templates/WrapIfAdditionalTemplate.tsx @@ -6,12 +6,14 @@ import { StrictRJSFSchema, WrapIfAdditionalTemplateProps, } from "@rjsf/utils"; +import { useEffect, useMemo, useState, type FocusEvent } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; import { LuTrash2 } from "react-icons/lu"; +import type { ConfigFormContext } from "@/types/configForm"; export function WrapIfAdditionalTemplate< T = unknown, @@ -30,6 +32,7 @@ export function WrapIfAdditionalTemplate< onKeyRenameBlur, readonly, required, + registry, schema, uiSchema, } = props; @@ -38,6 +41,55 @@ export function WrapIfAdditionalTemplate< const additional = ADDITIONAL_PROPERTY_FLAG in schema; + const uiOptions = getUiOptions(uiSchema); + const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true; + + const keyLabelKey = + typeof uiOptions.additionalPropertyKeyLabel === "string" + ? uiOptions.additionalPropertyKeyLabel + : undefined; + const keyPlaceholderKey = + typeof uiOptions.additionalPropertyKeyPlaceholder === "string" + ? uiOptions.additionalPropertyKeyPlaceholder + : undefined; + const keyPattern = + typeof uiOptions.additionalPropertyKeyPattern === "string" + ? uiOptions.additionalPropertyKeyPattern + : undefined; + const preventKeyRename = uiOptions.preventKeyRename === true; + + const formContext = registry?.formContext as ConfigFormContext | undefined; + + // optionally, lock the key once it's been saved + const baseline = formContext?.baselineFormData; + const keyLocked = + preventKeyRename && + typeof label === "string" && + !!baseline && + Object.prototype.hasOwnProperty.call(baseline, label); + + // controlled key value so we can validate live and block invalid renames. + const [keyValue, setKeyValue] = useState(label ?? ""); + useEffect(() => { + setKeyValue(label ?? ""); + }, [label]); + + const keyRegex = useMemo( + () => (keyPattern ? new RegExp(keyPattern) : undefined), + [keyPattern], + ); + const keyError = useMemo(() => { + if (!keyRegex || keyLocked) return null; + if (!keyRegex.test(keyValue)) { + return t("configForm.additionalProperties.keyPatternError", { + ns: "views/settings", + defaultValue: + "Use only letters, numbers, hyphens, and underscores (no spaces)", + }); + } + return null; + }, [keyRegex, keyLocked, keyValue, t]); + if (!additional) { return (
    @@ -47,20 +99,26 @@ export function WrapIfAdditionalTemplate< } const keyId = `${id}-key`; - const keyLabel = t("configForm.additionalProperties.keyLabel", { - ns: "views/settings", - }); + const keyLabel = keyLabelKey + ? t(keyLabelKey, { ns: "views/settings" }) + : t("configForm.additionalProperties.keyLabel", { ns: "views/settings" }); const valueLabel = t("configForm.additionalProperties.valueLabel", { ns: "views/settings", }); - const keyPlaceholder = t("configForm.additionalProperties.keyPlaceholder", { - ns: "views/settings", - }); + const keyPlaceholder = keyPlaceholderKey + ? t(keyPlaceholderKey, { ns: "views/settings" }) + : t("configForm.additionalProperties.keyPlaceholder", { + ns: "views/settings", + }); const removeLabel = t("configForm.additionalProperties.remove", { ns: "views/settings", }); - const uiOptions = getUiOptions(uiSchema); - const keyIsReadonly = uiOptions.additionalPropertyKeyReadonly === true; + + const commitKeyRename = (e: FocusEvent) => { + if (readonly) return; + if (keyError) return; + onKeyRenameBlur?.(e); + }; return (
    {displayLabel && } - {keyIsReadonly ? ( + {keyLocked ? (
    {label}
    ) : ( - + <> + setKeyValue(e.target.value)} + onBlur={!readonly ? commitKeyRename : undefined} + aria-invalid={keyError ? true : undefined} + /> + {keyError && ( +

    {keyError}

    + )} + )}
    )} diff --git a/web/src/components/config-form/theme/utils/index.ts b/web/src/components/config-form/theme/utils/index.ts index bb27f297b7..2c483ea231 100644 --- a/web/src/components/config-form/theme/utils/index.ts +++ b/web/src/components/config-form/theme/utils/index.ts @@ -17,3 +17,4 @@ export { isSubtreeModified, } from "./overrides"; export { getSizedFieldClassName } from "./fieldSizing"; +export { getNumericInputMode } from "./inputMode"; diff --git a/web/src/components/config-form/theme/utils/inputMode.ts b/web/src/components/config-form/theme/utils/inputMode.ts new file mode 100644 index 0000000000..02189e42d1 --- /dev/null +++ b/web/src/components/config-form/theme/utils/inputMode.ts @@ -0,0 +1,51 @@ +import type { RJSFSchema } from "@rjsf/utils"; + +type NumericInputOptions = { + signed?: boolean; +}; + +/** + * Derive the on-screen keyboard hint for a schema field. + * + * Numeric config fields render as text inputs because RJSF's NumberField + * relies on the widget echoing raw strings back, so that trailing "." and "0" + * characters survive while a value is being typed. That means the numeric + * keypad has to be requested explicitly. Desktop browsers ignore inputMode, so + * this only affects virtual keyboards. + * + * Fields accepting negative values opt out, since the iOS numeric and decimal + * keypads have no minus key. Most numeric fields declare no minimum even + * though they are non-negative, so signed fields are marked explicitly with + * ui:options.signed. + * + * Args: + * schema: The JSON schema for the field being rendered + * options: The resolved ui:options for the field + * + * Returns: + * The inputMode to apply, or undefined to leave the keyboard alone + */ +export function getNumericInputMode( + schema: RJSFSchema, + options: unknown, +): "numeric" | "decimal" | undefined { + const types = Array.isArray(schema.type) ? schema.type : [schema.type]; + const isInteger = types.includes("integer"); + + if (!isInteger && !types.includes("number")) { + return undefined; + } + + const numericOptions = + typeof options === "object" && options !== null + ? (options as NumericInputOptions) + : undefined; + + const minimum = schema.minimum ?? schema.exclusiveMinimum; + + if (numericOptions?.signed || (minimum ?? 0) < 0) { + return undefined; + } + + return isInteger ? "numeric" : "decimal"; +} diff --git a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx index e523bfd466..527789c814 100644 --- a/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx +++ b/web/src/components/config-form/theme/widgets/FfmpegArgsWidget.tsx @@ -120,6 +120,12 @@ export function FfmpegArgsWidget(props: WidgetProps) { id, } = props; const presetField = options?.ffmpegPresetField as PresetField | undefined; + // Path to this field within its config section. This is usually the same as + // the preset field, but the two diverge when the field sits below the + // section root: record.export.hwaccel_args uses the hwaccel_args preset list + // while living at export.hwaccel_args inside the record section. + const globalFieldPath = + (options?.ffmpegGlobalFieldPath as string | undefined) ?? presetField; const allowInherit = options?.allowInherit === true; const hideDescription = options?.hideDescription === true; const useSplitLayout = options?.splitLayout !== false; @@ -131,11 +137,18 @@ export function FfmpegArgsWidget(props: WidgetProps) { // Extract the global value for this specific field to detect inheritance const globalFieldValue = useMemo(() => { - if (!showUseGlobalSetting || !formContext?.globalValue || !presetField) { + if ( + !showUseGlobalSetting || + !formContext?.globalValue || + !globalFieldPath + ) { return undefined; } - return get(formContext.globalValue as Record, presetField); - }, [showUseGlobalSetting, formContext?.globalValue, presetField]); + return get( + formContext.globalValue as Record, + globalFieldPath, + ); + }, [showUseGlobalSetting, formContext?.globalValue, globalFieldPath]); const { data } = useSWR("ffmpeg/presets"); diff --git a/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx index 2d065faead..74fd206947 100644 --- a/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx @@ -1,8 +1,10 @@ import type { WidgetProps } from "@rjsf/utils"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; +import useSWR from "swr"; import { Switch } from "@/components/ui/switch"; import type { ConfigFormContext } from "@/types/configForm"; +import type { GenAIModelsResponse } from "@/types/chat"; const GENAI_ROLES = ["embeddings", "descriptions", "chat"] as const; @@ -37,10 +39,24 @@ export function GenAIRolesWidget(props: WidgetProps) { const selectedRoles = useMemo(() => normalizeValue(value), [value]); const providerKey = useMemo(() => getProviderKey(id), [id]); - // Compute occupied roles directly from formData. The computation is - // trivially cheap (iterate providers × 3 roles max) so we skip an - // intermediate memoization layer whose formData dependency would - // never produce a cache hit (new object reference on every change). + const { data: genaiInfo } = useSWR("genai/models", { + revalidateOnFocus: false, + }); + + const embeddingsSupported = useMemo(() => { + if (!providerKey) return true; + const info = genaiInfo?.[providerKey]; + return info ? info.supports_embeddings : true; + }, [genaiInfo, providerKey]); + + const availableRoles = useMemo( + () => + embeddingsSupported + ? GENAI_ROLES + : GENAI_ROLES.filter((role) => role !== "embeddings"), + [embeddingsSupported], + ); + const occupiedRoles = useMemo(() => { const occupied = new Set(); const fd = formContext?.formData; @@ -64,6 +80,12 @@ export function GenAIRolesWidget(props: WidgetProps) { return occupied; }, [formContext?.formData, providerKey]); + useEffect(() => { + if (!embeddingsSupported && selectedRoles.includes("embeddings")) { + onChange(selectedRoles.filter((role) => role !== "embeddings")); + } + }, [embeddingsSupported, selectedRoles, onChange]); + const toggleRole = (role: string, enabled: boolean) => { if (enabled) { if (!selectedRoles.includes(role)) { @@ -78,7 +100,7 @@ export function GenAIRolesWidget(props: WidgetProps) { return (
    - {GENAI_ROLES.map((role) => { + {availableRoles.map((role) => { const checked = selectedRoles.includes(role); const roleDisabled = !checked && occupiedRoles.has(role); const label = t(`configForm.genaiRoles.options.${role}`, { diff --git a/web/src/components/config-form/theme/widgets/TextWidget.tsx b/web/src/components/config-form/theme/widgets/TextWidget.tsx index 9c408797d1..b833a70618 100644 --- a/web/src/components/config-form/theme/widgets/TextWidget.tsx +++ b/web/src/components/config-form/theme/widgets/TextWidget.tsx @@ -2,7 +2,7 @@ import type { WidgetProps } from "@rjsf/utils"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; -import { getSizedFieldClassName } from "../utils"; +import { getNumericInputMode, getSizedFieldClassName } from "../utils"; export function TextWidget(props: WidgetProps) { const { @@ -28,6 +28,7 @@ export function TextWidget(props: WidgetProps) { id={id} className={cn(fieldClassName)} type="text" + inputMode={getNumericInputMode(schema, options)} value={value ?? ""} disabled={disabled || readonly} placeholder={placeholder || (options.placeholder as string) || ""} diff --git a/web/src/components/config-form/theme/widgets/ZoneSwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/ZoneSwitchesWidget.tsx index 0acb83b785..94ca405126 100644 --- a/web/src/components/config-form/theme/widgets/ZoneSwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/ZoneSwitchesWidget.tsx @@ -29,8 +29,8 @@ function getZoneDisplayName(zoneName: string, context?: FormContext): string { } } } - // Fallback to cleaning up the zone name - return String(zoneName).replace(/_/g, " "); + // Fallback to the raw zone id verbatim (no friendly_name available) + return String(zoneName); } export function ZoneSwitchesWidget(props: WidgetProps) { diff --git a/web/src/components/filter/ExportFilterGroup.tsx b/web/src/components/filter/ExportFilterGroup.tsx index c5fe4f33c6..9b1cfd7a7e 100644 --- a/web/src/components/filter/ExportFilterGroup.tsx +++ b/web/src/components/filter/ExportFilterGroup.tsx @@ -29,9 +29,13 @@ export default function ExportFilterGroup({ const filterValues = useMemo( () => ({ - cameras: allowedCameras, + cameras: [...allowedCameras].sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), }), - [allowedCameras], + [config, allowedCameras], ); const groups = useMemo(() => { diff --git a/web/src/components/filter/SearchFilterGroup.tsx b/web/src/components/filter/SearchFilterGroup.tsx index fe9a70e18f..a9fd0276aa 100644 --- a/web/src/components/filter/SearchFilterGroup.tsx +++ b/web/src/components/filter/SearchFilterGroup.tsx @@ -127,12 +127,16 @@ export default function SearchFilterGroup({ const filterValues = useMemo( () => ({ - cameras: allowedCameras, + cameras: [...allowedCameras].sort( + (a, b) => + (config?.cameras[a]?.ui?.order ?? 0) - + (config?.cameras[b]?.ui?.order ?? 0), + ), labels: Object.values(allLabels || {}), zones: Object.values(allZones || {}), search_type: ["thumbnail", "description"] as SearchSource[], }), - [allLabels, allZones, allowedCameras], + [config, allLabels, allZones, allowedCameras], ); const availableSortTypes = useMemo(() => { diff --git a/web/src/components/icons/IconPicker.tsx b/web/src/components/icons/IconPicker.tsx index a4775ae21b..73f64527c3 100644 --- a/web/src/components/icons/IconPicker.tsx +++ b/web/src/components/icons/IconPicker.tsx @@ -153,11 +153,15 @@ export default function IconPicker({ } type IconRendererProps = { - icon: IconType; + icon: IconType | undefined; size?: number; className?: string; }; export function IconRenderer({ icon, size, className }: IconRendererProps) { + if (!icon) { + return null; + } + return <>{React.createElement(icon, { size, className })}; } diff --git a/web/src/components/overlay/CameraInfoDialog.tsx b/web/src/components/overlay/CameraInfoDialog.tsx index fce1f6fd02..14755a2015 100644 --- a/web/src/components/overlay/CameraInfoDialog.tsx +++ b/web/src/components/overlay/CameraInfoDialog.tsx @@ -7,7 +7,8 @@ import { DialogTitle, } from "../ui/dialog"; import ActivityIndicator from "../indicators/activity-indicator"; -import { Ffprobe } from "@/types/stats"; +import KeyframeAnalysisSection from "./KeyframeAnalysisSection"; +import { Ffprobe, KeyframeAnalysis } from "@/types/stats"; import { Button } from "../ui/button"; import copy from "copy-to-clipboard"; import { CameraConfig } from "@/types/frigateConfig"; @@ -30,6 +31,7 @@ export default function CameraInfoDialog({ }: CameraInfoDialogProps) { const { t } = useTranslation(["views/system"]); const [ffprobeInfo, setFfprobeInfo] = useState(); + const [keyframeInfo, setKeyframeInfo] = useState(); useEffect(() => { axios @@ -67,7 +69,12 @@ export default function CameraInfoDialog({ }, []); const onCopyFfprobe = async () => { - copy(JSON.stringify(ffprobeInfo)); + copy( + JSON.stringify({ + ffprobe: ffprobeInfo, + keyframe_analysis: keyframeInfo, + }), + ); toast.success(t("cameras.toast.success.copyToClipboard")); }; @@ -96,7 +103,7 @@ export default function CameraInfoDialog({ cameras.info.streamDataFromFFPROBE -
    +
    {ffprobeInfo ? (
    {ffprobeInfo.map((stream, idx) => ( @@ -184,6 +191,10 @@ export default function CameraInfoDialog({ )}
    ))} +
    ) : (
    diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx index fef205ea48..317f635e3a 100644 --- a/web/src/components/overlay/CreateRoleDialog.tsx +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -53,9 +53,13 @@ export default function CreateRoleDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}).filter( - (name) => !isReplayCamera(name), - ); + const cameras = Object.keys(config.cameras || {}) + .filter((name) => !isReplayCamera(name)) + .sort( + (a, b) => + (config.cameras[a]?.ui?.order ?? 0) - + (config.cameras[b]?.ui?.order ?? 0), + ); const existingRoles = Object.keys(config.auth?.roles || {}); diff --git a/web/src/components/overlay/EditRoleCamerasDialog.tsx b/web/src/components/overlay/EditRoleCamerasDialog.tsx index cfa84db81f..1c70b6d102 100644 --- a/web/src/components/overlay/EditRoleCamerasDialog.tsx +++ b/web/src/components/overlay/EditRoleCamerasDialog.tsx @@ -47,9 +47,13 @@ export default function EditRoleCamerasDialog({ const { t } = useTranslation(["views/settings"]); const [isLoading, setIsLoading] = useState(false); - const cameras = Object.keys(config.cameras || {}).filter( - (name) => !isReplayCamera(name), - ); + const cameras = Object.keys(config.cameras || {}) + .filter((name) => !isReplayCamera(name)) + .sort( + (a, b) => + (config.cameras[a]?.ui?.order ?? 0) - + (config.cameras[b]?.ui?.order ?? 0), + ); const formSchema = z.object({ cameras: z diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 3419f199f9..0b0cb84c13 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -39,6 +39,16 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "../ui/command"; +import { IconRenderer } from "../icons/IconPicker"; +import * as LuIcons from "react-icons/lu"; import { isDesktop, isMobile } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import SaveExportOverlay from "./SaveExportOverlay"; @@ -55,6 +65,7 @@ import { Textarea } from "../ui/textarea"; import { useNavigate } from "react-router-dom"; import { useIsAdmin } from "@/hooks/use-is-admin"; import { isReplayCamera } from "@/utils/cameraUtil"; +import { isValidIconName } from "@/utils/iconUtil"; const EXPORT_OPTIONS = [ "1", @@ -124,7 +135,7 @@ export default function ExportDialog({ } if (!range) { - toast.error(t("export.toast.error.noVaildTimeSelected"), { + toast.error(t("export.toast.error.noValidTimeSelected"), { position: "top-center", }); return false; @@ -376,6 +387,9 @@ export function ExportContent({ const [newCaseName, setNewCaseName] = useState(""); const [newCaseDescription, setNewCaseDescription] = useState(""); const [isStartingBatchExport, setIsStartingBatchExport] = useState(false); + const [cameraSearch, setCameraSearch] = useState(""); + const [cameraMenuOpen, setCameraMenuOpen] = useState(false); + const cameraMenuRef = useRef(null); const multiRangeKey = useMemo(() => { if (activeTab !== "multi" || !range) { return undefined; @@ -577,13 +591,82 @@ export function ExportContent({ ); }, []); + const availableCameraIds = useMemo( + () => cameraActivities.map((activity) => activity.camera), + [cameraActivities], + ); + + const activeCameraIds = useMemo( + () => + cameraActivities + .filter((activity) => activity.hasDetections) + .map((activity) => activity.camera), + [cameraActivities], + ); + + const cameraGroups = useMemo( + () => + Object.entries(config?.camera_groups ?? {}) + .map(([name, group]) => ({ + name, + icon: group.icon, + order: group.order, + cameras: group.cameras.filter((cameraId) => + availableCameraIds.includes(cameraId), + ), + })) + .filter((group) => group.cameras.length > 0) + .sort((a, b) => a.order - b.order), + [config?.camera_groups, availableCameraIds], + ); + + // Filter the rendered camera cards by the search query + const filteredCameraActivities = useMemo(() => { + const query = cameraSearch.trim().toLowerCase(); + if (!query) { + return cameraActivities; + } + return cameraActivities.filter((activity) => { + const friendlyName = resolveCameraName(config, activity.camera); + return ( + activity.camera.toLowerCase().includes(query) || + friendlyName.toLowerCase().includes(query) + ); + }); + }, [cameraActivities, cameraSearch, config]); + + // Group/all/activity selection replaces the current selection + const applyCameraSelection = useCallback((cameraIds: string[]) => { + setHasManualCameraSelection(true); + setSelectedCameraIds(cameraIds); + setCameraMenuOpen(false); + }, []); + + // Close the dropdown when focus leaves the camera selection control entirely + const handleCameraInputBlur = useCallback((event: React.FocusEvent) => { + if ( + cameraMenuRef.current && + !cameraMenuRef.current.contains(event.relatedTarget as Node) + ) { + setCameraMenuOpen(false); + } + }, []); + + // Reset the search and dropdown when leaving the multi-camera tab + useEffect(() => { + if (activeTab !== "multi") { + setCameraSearch(""); + setCameraMenuOpen(false); + } + }, [activeTab]); + const startBatchExport = useCallback(async () => { if (isStartingBatchExport) { return; } if (!range) { - toast.error(t("export.toast.error.noVaildTimeSelected"), { + toast.error(t("export.toast.error.noValidTimeSelected"), { position: "top-center", }); return; @@ -802,7 +885,7 @@ export function ExportContent({ {isAdmin && (
    -