mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-02-05 00:15:51 +01:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
a43af33c62
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@ -75,15 +75,6 @@ jobs:
|
|||||||
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
|
rpi.tags=${{ steps.setup.outputs.image-name }}-rpi
|
||||||
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
||||||
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
|
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
|
||||||
- name: Build and push Rockchip build
|
|
||||||
uses: docker/bake-action@v3
|
|
||||||
with:
|
|
||||||
push: true
|
|
||||||
targets: rk
|
|
||||||
files: docker/rockchip/rk.hcl
|
|
||||||
set: |
|
|
||||||
rk.tags=${{ steps.setup.outputs.image-name }}-rk
|
|
||||||
*.cache-from=type=gha
|
|
||||||
jetson_jp4_build:
|
jetson_jp4_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Jetson Jetpack 4
|
name: Jetson Jetpack 4
|
||||||
|
2
.github/workflows/pull_request.yml
vendored
2
.github/workflows/pull_request.yml
vendored
@ -76,7 +76,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
appdirs==1.4.4
|
appdirs==1.4.*
|
||||||
argcomplete==2.0.0
|
argcomplete==2.0.*
|
||||||
contextlib2==0.6.0.post1
|
contextlib2==0.6.*
|
||||||
distlib==0.3.6
|
distlib==0.3.*
|
||||||
filelock==3.8.0
|
filelock==3.8.*
|
||||||
future==0.18.2
|
future==0.18.*
|
||||||
importlib-metadata==5.1.0
|
importlib-metadata==5.1.*
|
||||||
importlib-resources==5.1.2
|
importlib-resources==5.1.*
|
||||||
netaddr==0.8.0
|
netaddr==0.8.*
|
||||||
netifaces==0.10.9
|
netifaces==0.10.*
|
||||||
verboselogs==1.7
|
verboselogs==1.7.*
|
||||||
virtualenv==20.17.0
|
virtualenv==20.17.*
|
||||||
|
@ -12,26 +12,11 @@ ARG TARGETARCH
|
|||||||
COPY docker/tensorrt/requirements-amd64.txt /requirements-tensorrt.txt
|
COPY docker/tensorrt/requirements-amd64.txt /requirements-tensorrt.txt
|
||||||
RUN mkdir -p /trt-wheels && pip3 wheel --wheel-dir=/trt-wheels -r /requirements-tensorrt.txt
|
RUN mkdir -p /trt-wheels && pip3 wheel --wheel-dir=/trt-wheels -r /requirements-tensorrt.txt
|
||||||
|
|
||||||
# Build CuDNN
|
|
||||||
FROM wget AS cudnn-deps
|
|
||||||
|
|
||||||
ARG COMPUTE_LEVEL
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y git build-essential
|
|
||||||
|
|
||||||
RUN wget https://developer.download.nvidia.com/compute/cuda/repos/debian11/x86_64/cuda-keyring_1.1-1_all.deb \
|
|
||||||
&& dpkg -i cuda-keyring_1.1-1_all.deb \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get -y install cuda-toolkit \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
FROM tensorrt-base AS frigate-tensorrt
|
FROM tensorrt-base AS frigate-tensorrt
|
||||||
ENV TRT_VER=8.5.3
|
ENV TRT_VER=8.5.3
|
||||||
RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \
|
RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \
|
||||||
pip3 install -U /deps/trt-wheels/*.whl && \
|
pip3 install -U /deps/trt-wheels/*.whl && \
|
||||||
ldconfig
|
ldconfig
|
||||||
COPY --from=cudnn-deps /usr/local/cuda-12.6 /usr/local/cuda
|
|
||||||
|
|
||||||
ENV LD_LIBRARY_PATH=/usr/local/lib/python3.9/dist-packages/tensorrt:/usr/local/cuda/lib64:/usr/local/lib/python3.9/dist-packages/nvidia/cufft/lib
|
ENV LD_LIBRARY_PATH=/usr/local/lib/python3.9/dist-packages/tensorrt:/usr/local/cuda/lib64:/usr/local/lib/python3.9/dist-packages/nvidia/cufft/lib
|
||||||
WORKDIR /opt/frigate/
|
WORKDIR /opt/frigate/
|
||||||
@ -42,7 +27,7 @@ FROM devcontainer AS devcontainer-trt
|
|||||||
|
|
||||||
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
||||||
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
||||||
COPY --from=cudnn-deps /usr/local/cuda-12.6 /usr/local/cuda
|
COPY --from=trt-deps /usr/local/cuda-12.1 /usr/local/cuda
|
||||||
COPY docker/tensorrt/detector/rootfs/ /
|
COPY docker/tensorrt/detector/rootfs/ /
|
||||||
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
||||||
RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \
|
RUN --mount=type=bind,from=trt-wheels,source=/trt-wheels,target=/deps/trt-wheels \
|
||||||
|
@ -24,6 +24,7 @@ ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
|||||||
|
|
||||||
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
COPY --from=trt-deps /usr/local/lib/libyolo_layer.so /usr/local/lib/libyolo_layer.so
|
||||||
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
COPY --from=trt-deps /usr/local/src/tensorrt_demos /usr/local/src/tensorrt_demos
|
||||||
|
COPY --from=trt-deps /usr/local/cuda-12.* /usr/local/cuda
|
||||||
COPY docker/tensorrt/detector/rootfs/ /
|
COPY docker/tensorrt/detector/rootfs/ /
|
||||||
ENV YOLO_MODELS=""
|
ENV YOLO_MODELS=""
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ cameras:
|
|||||||
...
|
...
|
||||||
onvif:
|
onvif:
|
||||||
# Required: host of the camera being connected to.
|
# Required: host of the camera being connected to.
|
||||||
|
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
# Optional: ONVIF port for device (default: shown below).
|
# Optional: ONVIF port for device (default: shown below).
|
||||||
port: 8000
|
port: 8000
|
||||||
@ -49,6 +50,8 @@ cameras:
|
|||||||
user: admin
|
user: admin
|
||||||
# Optional: password for login.
|
# Optional: password for login.
|
||||||
password: admin
|
password: admin
|
||||||
|
# Optional: Skip TLS verification from the ONVIF server (default: shown below)
|
||||||
|
tls_insecure: False
|
||||||
# Optional: PTZ camera object autotracking. Keeps a moving object in
|
# Optional: PTZ camera object autotracking. Keeps a moving object in
|
||||||
# the center of the frame by automatically moving the PTZ camera.
|
# the center of the frame by automatically moving the PTZ camera.
|
||||||
autotracking:
|
autotracking:
|
||||||
|
@ -144,7 +144,9 @@ detectors:
|
|||||||
|
|
||||||
#### SSDLite MobileNet v2
|
#### 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. Use the model configuration shown below when using the OpenVINO detector with the default model.
|
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.
|
||||||
|
|
||||||
|
Use the model configuration shown below when using the OpenVINO detector with the default OpenVINO model:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
detectors:
|
detectors:
|
||||||
@ -254,6 +256,7 @@ yolov4x-mish-640
|
|||||||
yolov7-tiny-288
|
yolov7-tiny-288
|
||||||
yolov7-tiny-416
|
yolov7-tiny-416
|
||||||
yolov7-640
|
yolov7-640
|
||||||
|
yolov7-416
|
||||||
yolov7-320
|
yolov7-320
|
||||||
yolov7x-640
|
yolov7x-640
|
||||||
yolov7x-320
|
yolov7x-320
|
||||||
@ -282,6 +285,8 @@ The TensorRT detector can be selected by specifying `tensorrt` as the model type
|
|||||||
|
|
||||||
The TensorRT detector uses `.trt` model files that are located in `/config/model_cache/tensorrt` by default. These model path and dimensions used will depend on which model you have generated.
|
The TensorRT detector uses `.trt` model files that are located in `/config/model_cache/tensorrt` by default. These model path and dimensions used will depend on which model you have generated.
|
||||||
|
|
||||||
|
Use the config below to work with generated TRT models:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
detectors:
|
detectors:
|
||||||
tensorrt:
|
tensorrt:
|
||||||
|
@ -117,25 +117,27 @@ auth:
|
|||||||
hash_iterations: 600000
|
hash_iterations: 600000
|
||||||
|
|
||||||
# Optional: model modifications
|
# Optional: model modifications
|
||||||
|
# NOTE: The default values are for the EdgeTPU detector.
|
||||||
|
# Other detectors will require the model config to be set.
|
||||||
model:
|
model:
|
||||||
# Optional: path to the model (default: automatic based on detector)
|
# Required: path to the model (default: automatic based on detector)
|
||||||
path: /edgetpu_model.tflite
|
path: /edgetpu_model.tflite
|
||||||
# Optional: path to the labelmap (default: shown below)
|
# Required: path to the labelmap (default: shown below)
|
||||||
labelmap_path: /labelmap.txt
|
labelmap_path: /labelmap.txt
|
||||||
# Required: Object detection model input width (default: shown below)
|
# Required: Object detection model input width (default: shown below)
|
||||||
width: 320
|
width: 320
|
||||||
# Required: Object detection model input height (default: shown below)
|
# Required: Object detection model input height (default: shown below)
|
||||||
height: 320
|
height: 320
|
||||||
# Optional: Object detection model input colorspace
|
# Required: Object detection model input colorspace
|
||||||
# Valid values are rgb, bgr, or yuv. (default: shown below)
|
# Valid values are rgb, bgr, or yuv. (default: shown below)
|
||||||
input_pixel_format: rgb
|
input_pixel_format: rgb
|
||||||
# Optional: Object detection model input tensor format
|
# Required: Object detection model input tensor format
|
||||||
# Valid values are nhwc or nchw (default: shown below)
|
# Valid values are nhwc or nchw (default: shown below)
|
||||||
input_tensor: nhwc
|
input_tensor: nhwc
|
||||||
# Optional: Object detection model type, currently only used with the OpenVINO detector
|
# Required: Object detection model type, currently only used with the OpenVINO detector
|
||||||
# Valid values are ssd, yolox, yolonas (default: shown below)
|
# Valid values are ssd, yolox, yolonas (default: shown below)
|
||||||
model_type: ssd
|
model_type: ssd
|
||||||
# Optional: Label name modifications. These are merged into the standard labelmap.
|
# Required: Label name modifications. These are merged into the standard labelmap.
|
||||||
labelmap:
|
labelmap:
|
||||||
2: vehicle
|
2: vehicle
|
||||||
# Optional: Map of object labels to their attribute labels (default: depends on model)
|
# Optional: Map of object labels to their attribute labels (default: depends on model)
|
||||||
@ -686,6 +688,7 @@ cameras:
|
|||||||
# to enable PTZ controls.
|
# to enable PTZ controls.
|
||||||
onvif:
|
onvif:
|
||||||
# Required: host of the camera being connected to.
|
# Required: host of the camera being connected to.
|
||||||
|
# NOTE: HTTP is assumed by default; HTTPS is supported if you specify the scheme, ex: "https://0.0.0.0".
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
# Optional: ONVIF port for device (default: shown below).
|
# Optional: ONVIF port for device (default: shown below).
|
||||||
port: 8000
|
port: 8000
|
||||||
@ -694,6 +697,8 @@ cameras:
|
|||||||
user: admin
|
user: admin
|
||||||
# Optional: password for login.
|
# Optional: password for login.
|
||||||
password: admin
|
password: admin
|
||||||
|
# Optional: Skip TLS verification from the ONVIF server (default: shown below)
|
||||||
|
tls_insecure: False
|
||||||
# Optional: Ignores time synchronization mismatches between the camera and the server during authentication.
|
# Optional: Ignores time synchronization mismatches between the camera and the server during authentication.
|
||||||
# Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents.
|
# Using NTP on both ends is recommended and this should only be set to True in a "safe" environment due to the security risk it represents.
|
||||||
ignore_time_mismatch: False
|
ignore_time_mismatch: False
|
||||||
@ -757,6 +762,8 @@ cameras:
|
|||||||
- cat
|
- cat
|
||||||
# Optional: Restrict generation to objects that entered any of the listed zones (default: none, all zones qualify)
|
# Optional: Restrict generation to objects that entered any of the listed zones (default: none, all zones qualify)
|
||||||
required_zones: []
|
required_zones: []
|
||||||
|
# Optional: Save thumbnails sent to generative AI for review/debugging purposes (default: shown below)
|
||||||
|
debug_save_thumbnails: False
|
||||||
|
|
||||||
# Optional
|
# Optional
|
||||||
ui:
|
ui:
|
||||||
|
7069
docs/package-lock.json
generated
7069
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,15 +17,15 @@
|
|||||||
"write-heading-ids": "docusaurus write-heading-ids"
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.5.2",
|
"@docusaurus/core": "^3.6.3",
|
||||||
"@docusaurus/preset-classic": "^3.5.2",
|
"@docusaurus/preset-classic": "^3.6.3",
|
||||||
"@docusaurus/theme-mermaid": "^3.5.2",
|
"@docusaurus/theme-mermaid": "^3.6.3",
|
||||||
"@docusaurus/plugin-content-docs": "^3.5.2",
|
"@docusaurus/plugin-content-docs": "^3.6.3",
|
||||||
"@mdx-js/react": "^3.0.1",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"docusaurus-plugin-openapi-docs": "^4.1.0",
|
"docusaurus-plugin-openapi-docs": "^4.3.1",
|
||||||
"docusaurus-theme-openapi-docs": "^4.1.0",
|
"docusaurus-theme-openapi-docs": "^4.3.1",
|
||||||
"prism-react-renderer": "^2.4.0",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
|
@ -139,6 +139,8 @@ def config(request: Request):
|
|||||||
mode="json", warnings="none", exclude_none=True
|
mode="json", warnings="none", exclude_none=True
|
||||||
)
|
)
|
||||||
for stream_name, stream in go2rtc.get("streams", {}).items():
|
for stream_name, stream in go2rtc.get("streams", {}).items():
|
||||||
|
if stream is None:
|
||||||
|
continue
|
||||||
if isinstance(stream, str):
|
if isinstance(stream, str):
|
||||||
cleaned = clean_camera_user_pass(stream)
|
cleaned = clean_camera_user_pass(stream)
|
||||||
else:
|
else:
|
||||||
|
@ -437,7 +437,7 @@ class FrigateApp:
|
|||||||
# pre-create shms
|
# pre-create shms
|
||||||
for i in range(shm_frame_count):
|
for i in range(shm_frame_count):
|
||||||
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
|
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
|
||||||
self.frame_manager.create(f"{config.name}_{i}", frame_size)
|
self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
|
||||||
|
|
||||||
capture_process = util.Process(
|
capture_process = util.Process(
|
||||||
target=capture_camera,
|
target=capture_camera,
|
||||||
|
@ -38,6 +38,10 @@ class GenAICameraConfig(BaseModel):
|
|||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="List of required zones to be entered in order to run generative AI.",
|
title="List of required zones to be entered in order to run generative AI.",
|
||||||
)
|
)
|
||||||
|
debug_save_thumbnails: bool = Field(
|
||||||
|
default=False,
|
||||||
|
title="Save thumbnails sent to generative AI for debugging purposes.",
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("required_zones", mode="before")
|
@field_validator("required_zones", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -74,6 +74,7 @@ class OnvifConfig(FrigateBaseModel):
|
|||||||
port: int = Field(default=8000, title="Onvif Port")
|
port: int = Field(default=8000, title="Onvif Port")
|
||||||
user: Optional[EnvString] = Field(default=None, title="Onvif Username")
|
user: Optional[EnvString] = Field(default=None, title="Onvif Username")
|
||||||
password: Optional[EnvString] = Field(default=None, title="Onvif Password")
|
password: Optional[EnvString] = Field(default=None, title="Onvif Password")
|
||||||
|
tls_insecure: bool = Field(default=False, title="Onvif Disable TLS verification")
|
||||||
autotracking: PtzAutotrackConfig = Field(
|
autotracking: PtzAutotrackConfig = Field(
|
||||||
default_factory=PtzAutotrackConfig,
|
default_factory=PtzAutotrackConfig,
|
||||||
title="PTZ auto tracking config.",
|
title="PTZ auto tracking config.",
|
||||||
|
@ -4,6 +4,7 @@ from typing import Optional
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from frigate.const import MAX_PRE_CAPTURE
|
from frigate.const import MAX_PRE_CAPTURE
|
||||||
|
from frigate.review.types import SeverityEnum
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
|
||||||
@ -101,3 +102,15 @@ class RecordConfig(FrigateBaseModel):
|
|||||||
self.alerts.pre_capture,
|
self.alerts.pre_capture,
|
||||||
self.detections.pre_capture,
|
self.detections.pre_capture,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_review_pre_capture(self, severity: SeverityEnum) -> int:
|
||||||
|
if severity == SeverityEnum.alert:
|
||||||
|
return self.alerts.pre_capture
|
||||||
|
else:
|
||||||
|
return self.detections.pre_capture
|
||||||
|
|
||||||
|
def get_review_post_capture(self, severity: SeverityEnum) -> int:
|
||||||
|
if severity == SeverityEnum.alert:
|
||||||
|
return self.alerts.post_capture
|
||||||
|
else:
|
||||||
|
return self.detections.post_capture
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
@ -217,6 +218,8 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
_, buffer = cv2.imencode(".jpg", cropped_image)
|
_, buffer = cv2.imencode(".jpg", cropped_image)
|
||||||
snapshot_image = buffer.tobytes()
|
snapshot_image = buffer.tobytes()
|
||||||
|
|
||||||
|
num_thumbnails = len(self.tracked_events.get(event_id, []))
|
||||||
|
|
||||||
embed_image = (
|
embed_image = (
|
||||||
[snapshot_image]
|
[snapshot_image]
|
||||||
if event.has_snapshot and camera_config.genai.use_snapshot
|
if event.has_snapshot and camera_config.genai.use_snapshot
|
||||||
@ -225,11 +228,37 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
data["thumbnail"]
|
data["thumbnail"]
|
||||||
for data in self.tracked_events[event_id]
|
for data in self.tracked_events[event_id]
|
||||||
]
|
]
|
||||||
if len(self.tracked_events.get(event_id, [])) > 0
|
if num_thumbnails > 0
|
||||||
else [thumbnail]
|
else [thumbnail]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if camera_config.genai.debug_save_thumbnails and num_thumbnails > 0:
|
||||||
|
logger.debug(
|
||||||
|
f"Saving {num_thumbnails} thumbnails for event {event.id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
Path(
|
||||||
|
os.path.join(CLIPS_DIR, f"genai-requests/{event.id}")
|
||||||
|
).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
for idx, data in enumerate(self.tracked_events[event_id], 1):
|
||||||
|
jpg_bytes: bytes = data["thumbnail"]
|
||||||
|
|
||||||
|
if jpg_bytes is None:
|
||||||
|
logger.warning(
|
||||||
|
f"Unable to save thumbnail {idx} for {event.id}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with open(
|
||||||
|
os.path.join(
|
||||||
|
CLIPS_DIR,
|
||||||
|
f"genai-requests/{event.id}/{idx}.jpg",
|
||||||
|
),
|
||||||
|
"wb",
|
||||||
|
) as j:
|
||||||
|
j.write(jpg_bytes)
|
||||||
|
|
||||||
# Generate the description. Call happens in a thread since it is network bound.
|
# Generate the description. Call happens in a thread since it is network bound.
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._embed_description,
|
target=self._embed_description,
|
||||||
|
@ -121,8 +121,8 @@ class EventCleanup(threading.Thread):
|
|||||||
|
|
||||||
events_to_update = []
|
events_to_update = []
|
||||||
|
|
||||||
for batch in query.iterator():
|
for event in query.iterator():
|
||||||
events_to_update.extend([event.id for event in batch])
|
events_to_update.append(event.id)
|
||||||
if len(events_to_update) >= CHUNK_SIZE:
|
if len(events_to_update) >= CHUNK_SIZE:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Updating {update_params} for {len(events_to_update)} events"
|
f"Updating {update_params} for {len(events_to_update)} events"
|
||||||
@ -257,7 +257,7 @@ class EventCleanup(threading.Thread):
|
|||||||
events_to_update = []
|
events_to_update = []
|
||||||
|
|
||||||
for event in query.iterator():
|
for event in query.iterator():
|
||||||
events_to_update.append(event)
|
events_to_update.append(event.id)
|
||||||
|
|
||||||
if len(events_to_update) >= CHUNK_SIZE:
|
if len(events_to_update) >= CHUNK_SIZE:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -6,6 +6,7 @@ from importlib.util import find_spec
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
import requests
|
||||||
from onvif import ONVIFCamera, ONVIFError
|
from onvif import ONVIFCamera, ONVIFError
|
||||||
from zeep.exceptions import Fault, TransportError
|
from zeep.exceptions import Fault, TransportError
|
||||||
from zeep.transports import Transport
|
from zeep.transports import Transport
|
||||||
@ -48,7 +49,11 @@ class OnvifController:
|
|||||||
|
|
||||||
if cam.onvif.host:
|
if cam.onvif.host:
|
||||||
try:
|
try:
|
||||||
transport = Transport(timeout=10, operation_timeout=10)
|
session = requests.Session()
|
||||||
|
session.verify = not cam.onvif.tls_insecure
|
||||||
|
transport = Transport(
|
||||||
|
timeout=10, operation_timeout=10, session=session
|
||||||
|
)
|
||||||
self.cams[cam_name] = {
|
self.cams[cam_name] = {
|
||||||
"onvif": ONVIFCamera(
|
"onvif": ONVIFCamera(
|
||||||
cam.onvif.host,
|
cam.onvif.host,
|
||||||
@ -558,6 +563,7 @@ class OnvifController:
|
|||||||
if not self._init_onvif(camera_name):
|
if not self._init_onvif(camera_name):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
if command == OnvifCommandEnum.init:
|
if command == OnvifCommandEnum.init:
|
||||||
# already init
|
# already init
|
||||||
return
|
return
|
||||||
@ -569,11 +575,14 @@ class OnvifController:
|
|||||||
_, pan, tilt = param.split("_")
|
_, pan, tilt = param.split("_")
|
||||||
self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
|
self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
|
||||||
elif (
|
elif (
|
||||||
command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out
|
command == OnvifCommandEnum.zoom_in
|
||||||
|
or command == OnvifCommandEnum.zoom_out
|
||||||
):
|
):
|
||||||
self._zoom(camera_name, command)
|
self._zoom(camera_name, command)
|
||||||
else:
|
else:
|
||||||
self._move(camera_name, command)
|
self._move(camera_name, command)
|
||||||
|
except ONVIFError as e:
|
||||||
|
logger.error(f"Unable to handle onvif command: {e}")
|
||||||
|
|
||||||
def get_camera_info(self, camera_name: str) -> dict[str, any]:
|
def get_camera_info(self, camera_name: str) -> dict[str, any]:
|
||||||
if camera_name not in self.cams.keys():
|
if camera_name not in self.cams.keys():
|
||||||
|
@ -29,6 +29,7 @@ from frigate.const import (
|
|||||||
RECORD_DIR,
|
RECORD_DIR,
|
||||||
)
|
)
|
||||||
from frigate.models import Recordings, ReviewSegment
|
from frigate.models import Recordings, ReviewSegment
|
||||||
|
from frigate.review.types import SeverityEnum
|
||||||
from frigate.util.services import get_video_properties
|
from frigate.util.services import get_video_properties
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -194,6 +195,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
ReviewSegment.select(
|
ReviewSegment.select(
|
||||||
ReviewSegment.start_time,
|
ReviewSegment.start_time,
|
||||||
ReviewSegment.end_time,
|
ReviewSegment.end_time,
|
||||||
|
ReviewSegment.severity,
|
||||||
ReviewSegment.data,
|
ReviewSegment.data,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
@ -219,11 +221,15 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
[r for r in recordings_to_insert if r is not None],
|
[r for r in recordings_to_insert if r is not None],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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(
|
async def validate_and_move_segment(
|
||||||
self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any]
|
self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any]
|
||||||
) -> None:
|
) -> None:
|
||||||
cache_path = recording["cache_path"]
|
cache_path: str = recording["cache_path"]
|
||||||
start_time = recording["start_time"]
|
start_time: datetime.datetime = recording["start_time"]
|
||||||
record_config = self.config.cameras[camera].record
|
record_config = self.config.cameras[camera].record
|
||||||
|
|
||||||
# Just delete files if recordings are turned off
|
# Just delete files if recordings are turned off
|
||||||
@ -231,8 +237,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
camera not in self.config.cameras
|
camera not in self.config.cameras
|
||||||
or not self.config.cameras[camera].record.enabled
|
or not self.config.cameras[camera].record.enabled
|
||||||
):
|
):
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
self.drop_segment(cache_path)
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if cache_path in self.end_time_cache:
|
if cache_path in self.end_time_cache:
|
||||||
@ -260,24 +265,34 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# if cached file's start_time is earlier than the retain days for the camera
|
# if cached file's start_time is earlier than the retain days for the camera
|
||||||
|
# meaning continuous recording is not enabled
|
||||||
if start_time <= (
|
if start_time <= (
|
||||||
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
datetime.datetime.now().astimezone(datetime.timezone.utc)
|
||||||
- datetime.timedelta(days=self.config.cameras[camera].record.retain.days)
|
- datetime.timedelta(days=self.config.cameras[camera].record.retain.days)
|
||||||
):
|
):
|
||||||
# if the cached segment overlaps with the events:
|
# if the cached segment overlaps with the review items:
|
||||||
overlaps = False
|
overlaps = False
|
||||||
for review in reviews:
|
for review in reviews:
|
||||||
# if the event starts in the future, stop checking events
|
severity = SeverityEnum[review.severity]
|
||||||
|
|
||||||
|
# if the review item starts in the future, stop checking review items
|
||||||
# and remove this segment
|
# and remove this segment
|
||||||
if review.start_time > end_time.timestamp():
|
if (
|
||||||
|
review.start_time - record_config.get_review_pre_capture(severity)
|
||||||
|
) > end_time.timestamp():
|
||||||
overlaps = False
|
overlaps = False
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# if the event is in progress or ends after the recording starts, keep it
|
# if the review item is in progress or ends after the recording starts, keep it
|
||||||
# and stop looking at events
|
# and stop looking at review items
|
||||||
if review.end_time is None or review.end_time >= start_time.timestamp():
|
if (
|
||||||
|
review.end_time is None
|
||||||
|
or (
|
||||||
|
review.end_time
|
||||||
|
+ record_config.get_review_post_capture(severity)
|
||||||
|
)
|
||||||
|
>= start_time.timestamp()
|
||||||
|
):
|
||||||
overlaps = True
|
overlaps = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -296,7 +311,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
cache_path,
|
cache_path,
|
||||||
record_mode,
|
record_mode,
|
||||||
)
|
)
|
||||||
# if it doesn't overlap with an event, go ahead and drop the segment
|
# if it doesn't overlap with an review item, go ahead and drop the segment
|
||||||
# if it ends more than the configured pre_capture for the camera
|
# if it ends more than the configured pre_capture for the camera
|
||||||
else:
|
else:
|
||||||
camera_info = self.object_recordings_info[camera]
|
camera_info = self.object_recordings_info[camera]
|
||||||
@ -307,9 +322,9 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
most_recently_processed_frame_time - record_config.event_pre_capture
|
most_recently_processed_frame_time - record_config.event_pre_capture
|
||||||
).astimezone(datetime.timezone.utc)
|
).astimezone(datetime.timezone.utc)
|
||||||
if end_time < retain_cutoff:
|
if end_time < retain_cutoff:
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
self.drop_segment(cache_path)
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
# else retain days includes this segment
|
# else retain days includes this segment
|
||||||
|
# meaning continuous recording is enabled
|
||||||
else:
|
else:
|
||||||
# assume that empty means the relevant recording info has not been received yet
|
# assume that empty means the relevant recording info has not been received yet
|
||||||
camera_info = self.object_recordings_info[camera]
|
camera_info = self.object_recordings_info[camera]
|
||||||
@ -390,8 +405,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
|
|
||||||
# check if the segment shouldn't be stored
|
# check if the segment shouldn't be stored
|
||||||
if segment_info.should_discard_segment(store_mode):
|
if segment_info.should_discard_segment(store_mode):
|
||||||
Path(cache_path).unlink(missing_ok=True)
|
self.drop_segment(cache_path)
|
||||||
self.end_time_cache.pop(cache_path, None)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# directory will be in utc due to start_time being in utc
|
# directory will be in utc due to start_time being in utc
|
||||||
|
@ -293,7 +293,7 @@ def stats_snapshot(
|
|||||||
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:
|
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:
|
||||||
try:
|
try:
|
||||||
storage_stats = shutil.disk_usage(path)
|
storage_stats = shutil.disk_usage(path)
|
||||||
except FileNotFoundError:
|
except (FileNotFoundError, OSError):
|
||||||
stats["service"]["storage"][path] = {}
|
stats["service"]["storage"][path] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ def capture_frames(
|
|||||||
fps.value = frame_rate.eps()
|
fps.value = frame_rate.eps()
|
||||||
skipped_fps.value = skipped_eps.eps()
|
skipped_fps.value = skipped_eps.eps()
|
||||||
current_frame.value = datetime.datetime.now().timestamp()
|
current_frame.value = datetime.datetime.now().timestamp()
|
||||||
frame_name = f"{config.name}_{frame_index}"
|
frame_name = f"{config.name}_frame{frame_index}"
|
||||||
frame_buffer = frame_manager.write(frame_name)
|
frame_buffer = frame_manager.write(frame_name)
|
||||||
try:
|
try:
|
||||||
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
||||||
|
@ -755,7 +755,11 @@ export function CameraGroupEdit({
|
|||||||
<FormMessage />
|
<FormMessage />
|
||||||
{[
|
{[
|
||||||
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
...(birdseyeConfig?.enabled ? ["birdseye"] : []),
|
||||||
...Object.keys(config?.cameras ?? {}),
|
...Object.keys(config?.cameras ?? {}).sort(
|
||||||
|
(a, b) =>
|
||||||
|
(config?.cameras[a]?.ui?.order ?? 0) -
|
||||||
|
(config?.cameras[b]?.ui?.order ?? 0),
|
||||||
|
),
|
||||||
].map((camera) => (
|
].map((camera) => (
|
||||||
<FormControl key={camera}>
|
<FormControl key={camera}>
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
|
@ -477,7 +477,10 @@ export default function ObjectLifecycle({
|
|||||||
</p>
|
</p>
|
||||||
{Array.isArray(item.data.box) &&
|
{Array.isArray(item.data.box) &&
|
||||||
item.data.box.length >= 4
|
item.data.box.length >= 4
|
||||||
? (item.data.box[2] / item.data.box[3]).toFixed(2)
|
? (
|
||||||
|
aspectRatio *
|
||||||
|
(item.data.box[2] / item.data.box[3])
|
||||||
|
).toFixed(2)
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -505,7 +505,6 @@ function ObjectDetailsTab({
|
|||||||
|
|
||||||
<div className="flex w-full flex-row justify-end gap-2">
|
<div className="flex w-full flex-row justify-end gap-2">
|
||||||
{config?.cameras[search.camera].genai.enabled && search.end_time && (
|
{config?.cameras[search.camera].genai.enabled && search.end_time && (
|
||||||
<>
|
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<Button
|
<Button
|
||||||
className="rounded-r-none border-r-0"
|
className="rounded-r-none border-r-0"
|
||||||
@ -543,7 +542,9 @@ function ObjectDetailsTab({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{(config?.cameras[search.camera].genai.enabled && search.end_time) ||
|
||||||
|
(!config?.cameras[search.camera].genai.enabled && (
|
||||||
<Button
|
<Button
|
||||||
variant="select"
|
variant="select"
|
||||||
aria-label="Save"
|
aria-label="Save"
|
||||||
@ -551,8 +552,7 @@ function ObjectDetailsTab({
|
|||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,7 @@ export default function SearchSettings({
|
|||||||
const trigger = (
|
const trigger = (
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
aria-label="Search Settings"
|
aria-label="Explore Settings"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<FaCog className="text-secondary-foreground" />
|
<FaCog className="text-secondary-foreground" />
|
||||||
|
@ -328,12 +328,12 @@ export default function Explore() {
|
|||||||
<div className="flex max-w-96 flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5">
|
<div className="flex max-w-96 flex-col items-center justify-center space-y-3 rounded-lg bg-background/50 p-5">
|
||||||
<div className="my-5 flex flex-col items-center gap-2 text-xl">
|
<div className="my-5 flex flex-col items-center gap-2 text-xl">
|
||||||
<TbExclamationCircle className="mb-3 size-10" />
|
<TbExclamationCircle className="mb-3 size-10" />
|
||||||
<div>Search Unavailable</div>
|
<div>Explore is Unavailable</div>
|
||||||
</div>
|
</div>
|
||||||
{embeddingsReindexing && allModelsLoaded && (
|
{embeddingsReindexing && allModelsLoaded && (
|
||||||
<>
|
<>
|
||||||
<div className="text-center text-primary-variant">
|
<div className="text-center text-primary-variant">
|
||||||
Search can be used after tracked object embeddings have
|
Explore can be used after tracked object embeddings have
|
||||||
finished reindexing.
|
finished reindexing.
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-5 text-center">
|
<div className="pt-5 text-center">
|
||||||
@ -384,8 +384,8 @@ export default function Explore() {
|
|||||||
<>
|
<>
|
||||||
<div className="text-center text-primary-variant">
|
<div className="text-center text-primary-variant">
|
||||||
Frigate is downloading the necessary embeddings models to
|
Frigate is downloading the necessary embeddings models to
|
||||||
support semantic searching. This may take several minutes
|
support the Semantic Search feature. This may take several
|
||||||
depending on the speed of your network connection.
|
minutes depending on the speed of your network connection.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-96 flex-col gap-2 py-5">
|
<div className="flex w-96 flex-col gap-2 py-5">
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
|
@ -40,7 +40,7 @@ import UiSettingsView from "@/views/settings/UiSettingsView";
|
|||||||
|
|
||||||
const allSettingsViews = [
|
const allSettingsViews = [
|
||||||
"UI settings",
|
"UI settings",
|
||||||
"search settings",
|
"explore settings",
|
||||||
"camera settings",
|
"camera settings",
|
||||||
"masks / zones",
|
"masks / zones",
|
||||||
"motion tuner",
|
"motion tuner",
|
||||||
@ -175,7 +175,7 @@ export default function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
|
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
|
||||||
{page == "UI settings" && <UiSettingsView />}
|
{page == "UI settings" && <UiSettingsView />}
|
||||||
{page == "search settings" && (
|
{page == "explore settings" && (
|
||||||
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
|
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
|
||||||
)}
|
)}
|
||||||
{page == "debug" && (
|
{page == "debug" && (
|
||||||
|
@ -142,6 +142,7 @@ export interface CameraConfig {
|
|||||||
password: string | null;
|
password: string | null;
|
||||||
port: number;
|
port: number;
|
||||||
user: string | null;
|
user: string | null;
|
||||||
|
tls_insecure: boolean;
|
||||||
};
|
};
|
||||||
record: {
|
record: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -17,7 +17,12 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { useResizeObserver } from "@/hooks/resize-observer";
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
import useKeyboardListener from "@/hooks/use-keyboard-listener";
|
||||||
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
|
||||||
@ -29,6 +34,7 @@ import {
|
|||||||
import { CameraPtzInfo } from "@/types/ptz";
|
import { CameraPtzInfo } from "@/types/ptz";
|
||||||
import { RecordingStartingPoint } from "@/types/record";
|
import { RecordingStartingPoint } from "@/types/record";
|
||||||
import React, {
|
import React, {
|
||||||
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@ -518,6 +524,53 @@ export default function LiveCameraView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TooltipButtonProps = {
|
||||||
|
label: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
onMouseDown?: (e: React.MouseEvent) => void;
|
||||||
|
onMouseUp?: (e: React.MouseEvent) => void;
|
||||||
|
onTouchStart?: (e: React.TouchEvent) => void;
|
||||||
|
onTouchEnd?: (e: React.TouchEvent) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TooltipButton({
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseUp,
|
||||||
|
onTouchStart,
|
||||||
|
onTouchEnd,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: TooltipButtonProps) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label={label}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onTouchStart={onTouchStart}
|
||||||
|
onTouchEnd={onTouchEnd}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function PtzControlPanel({
|
function PtzControlPanel({
|
||||||
camera,
|
camera,
|
||||||
clickOverlay,
|
clickOverlay,
|
||||||
@ -611,8 +664,8 @@ function PtzControlPanel({
|
|||||||
>
|
>
|
||||||
{ptz?.features?.includes("pt") && (
|
{ptz?.features?.includes("pt") && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Move PTZ camera to the left"
|
label="Move camera left"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_LEFT");
|
sendPtz("MOVE_LEFT");
|
||||||
@ -625,9 +678,9 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<FaAngleLeft />
|
<FaAngleLeft />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Move PTZ camera up"
|
label="Move camera up"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_UP");
|
sendPtz("MOVE_UP");
|
||||||
@ -640,9 +693,9 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<FaAngleUp />
|
<FaAngleUp />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Move PTZ camera down"
|
label="Move camera down"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_DOWN");
|
sendPtz("MOVE_DOWN");
|
||||||
@ -655,9 +708,9 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<FaAngleDown />
|
<FaAngleDown />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Move PTZ camera to the right"
|
label="Move camera right"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("MOVE_RIGHT");
|
sendPtz("MOVE_RIGHT");
|
||||||
@ -670,13 +723,13 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<FaAngleRight />
|
<FaAngleRight />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ptz?.features?.includes("zoom") && (
|
{ptz?.features?.includes("zoom") && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Zoom PTZ camera in"
|
label="Zoom in"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("ZOOM_IN");
|
sendPtz("ZOOM_IN");
|
||||||
@ -689,9 +742,9 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<MdZoomIn />
|
<MdZoomIn />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
<Button
|
<TooltipButton
|
||||||
aria-label="Zoom PTZ camera out"
|
label="Zoom out"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendPtz("ZOOM_OUT");
|
sendPtz("ZOOM_OUT");
|
||||||
@ -704,21 +757,32 @@ function PtzControlPanel({
|
|||||||
onTouchEnd={onStop}
|
onTouchEnd={onStop}
|
||||||
>
|
>
|
||||||
<MdZoomOut />
|
<MdZoomOut />
|
||||||
</Button>
|
</TooltipButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ptz?.features?.includes("pt-r-fov") && (
|
{ptz?.features?.includes("pt-r-fov") && (
|
||||||
<>
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
className={`${clickOverlay ? "text-selected" : "text-primary"}`}
|
||||||
aria-label="Click in the frame to center the PTZ camera"
|
aria-label="Click in the frame to center the camera"
|
||||||
onClick={() => setClickOverlay(!clickOverlay)}
|
onClick={() => setClickOverlay(!clickOverlay)}
|
||||||
>
|
>
|
||||||
<TbViewfinder />
|
<TbViewfinder />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{clickOverlay ? "Disable" : "Enable"} click to move</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
{(ptz?.presets?.length ?? 0) > 0 && (
|
{(ptz?.presets?.length ?? 0) > 0 && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<DropdownMenu modal={!isDesktop}>
|
<DropdownMenu modal={!isDesktop}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button aria-label="PTZ camera presets">
|
<Button aria-label="PTZ camera presets">
|
||||||
@ -729,8 +793,7 @@ function PtzControlPanel({
|
|||||||
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
|
className="scrollbar-container max-h-[40dvh] overflow-y-auto"
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
{ptz?.presets.map((preset) => {
|
{ptz?.presets.map((preset) => (
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={preset}
|
key={preset}
|
||||||
aria-label={preset}
|
aria-label={preset}
|
||||||
@ -739,10 +802,15 @@ function PtzControlPanel({
|
|||||||
>
|
>
|
||||||
{preset}
|
{preset}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>PTZ camera presets</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -91,7 +91,7 @@ export default function SearchSettingsView({
|
|||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Search settings have been saved.", {
|
toast.success("Explore settings have been saved.", {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
setChangedValue(false);
|
setChangedValue(false);
|
||||||
@ -128,7 +128,7 @@ export default function SearchSettingsView({
|
|||||||
if (changedValue) {
|
if (changedValue) {
|
||||||
addMessage(
|
addMessage(
|
||||||
"search_settings",
|
"search_settings",
|
||||||
`Unsaved search settings changes`,
|
`Unsaved Explore settings changes`,
|
||||||
undefined,
|
undefined,
|
||||||
"search_settings",
|
"search_settings",
|
||||||
);
|
);
|
||||||
@ -140,7 +140,7 @@ export default function SearchSettingsView({
|
|||||||
}, [changedValue]);
|
}, [changedValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Search Settings - Frigate";
|
document.title = "Explore Settings - Frigate";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -152,7 +152,7 @@ export default function SearchSettingsView({
|
|||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||||
<Heading as="h3" className="my-2">
|
<Heading as="h3" className="my-2">
|
||||||
Search Settings
|
Explore Settings
|
||||||
</Heading>
|
</Heading>
|
||||||
<Separator className="my-2 flex bg-secondary" />
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
<Heading as="h4" className="my-2">
|
<Heading as="h4" className="my-2">
|
||||||
@ -221,7 +221,7 @@ export default function SearchSettingsView({
|
|||||||
<div className="text-md">Model Size</div>
|
<div className="text-md">Model Size</div>
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
<div className="space-y-1 text-sm text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
The size of the model used for semantic search embeddings.
|
The size of the model used for Semantic Search embeddings.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-5 text-sm">
|
<ul className="list-disc pl-5 text-sm">
|
||||||
<li>
|
<li>
|
||||||
|
Loading…
Reference in New Issue
Block a user