mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 14:18:21 +03:00
Merge branch 'blakeblackshear:dev' into dev
This commit is contained in:
commit
86de033f74
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push amd64 standard build
|
- name: Build and push amd64 standard build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/main/Dockerfile
|
file: docker/main/Dockerfile
|
||||||
@ -56,7 +56,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push arm64 standard build
|
- name: Build and push arm64 standard build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/main/Dockerfile
|
file: docker/main/Dockerfile
|
||||||
@ -67,7 +67,7 @@ jobs:
|
|||||||
${{ steps.setup.outputs.image-name }}-standard-arm64
|
${{ steps.setup.outputs.image-name }}-standard-arm64
|
||||||
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
|
||||||
- name: Build and push RPi build
|
- name: Build and push RPi build
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
@ -96,7 +96,7 @@ jobs:
|
|||||||
BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
||||||
SLIM_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
SLIM_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
||||||
TRT_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
TRT_BASE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
@ -124,7 +124,7 @@ jobs:
|
|||||||
- name: Build and push TensorRT (x86 GPU)
|
- name: Build and push TensorRT (x86 GPU)
|
||||||
env:
|
env:
|
||||||
COMPUTE_LEVEL: "50 60 70 80 90"
|
COMPUTE_LEVEL: "50 60 70 80 90"
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
@ -137,7 +137,7 @@ jobs:
|
|||||||
- name: AMD/ROCm general build
|
- name: AMD/ROCm general build
|
||||||
env:
|
env:
|
||||||
HSA_OVERRIDE: 0
|
HSA_OVERRIDE: 0
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
@ -163,7 +163,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push Rockchip build
|
- name: Build and push Rockchip build
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
@ -188,7 +188,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push Synaptics build
|
- name: Build and push Synaptics build
|
||||||
uses: docker/bake-action@v6
|
uses: docker/bake-action@v7
|
||||||
with:
|
with:
|
||||||
source: .
|
source: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,6 +3,8 @@ __pycache__
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
*.swp
|
*.swp
|
||||||
debug
|
debug
|
||||||
|
.claude/*
|
||||||
|
.mcp.json
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
config/*
|
config/*
|
||||||
@ -19,4 +21,4 @@ web/.env
|
|||||||
core
|
core
|
||||||
!/web/**/*.ts
|
!/web/**/*.ts
|
||||||
.idea/*
|
.idea/*
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|||||||
@ -266,6 +266,12 @@ RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
|||||||
RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \
|
RUN --mount=type=bind,from=wheels,source=/wheels,target=/deps/wheels \
|
||||||
pip3 install -U /deps/wheels/*.whl
|
pip3 install -U /deps/wheels/*.whl
|
||||||
|
|
||||||
|
# Install Axera Engine
|
||||||
|
RUN pip3 install https://github.com/AXERA-TECH/pyaxengine/releases/download/0.1.3-frigate/axengine-0.1.3-py3-none-any.whl
|
||||||
|
|
||||||
|
ENV PATH="${PATH}:/usr/bin/axcl"
|
||||||
|
ENV LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:/usr/lib/axcl"
|
||||||
|
|
||||||
# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image)
|
# Install MemryX runtime (requires libgomp (OpenMP) in the final docker image)
|
||||||
RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \
|
RUN --mount=type=bind,source=docker/main/install_memryx.sh,target=/deps/install_memryx.sh \
|
||||||
bash -c "bash /deps/install_memryx.sh"
|
bash -c "bash /deps/install_memryx.sh"
|
||||||
|
|||||||
@ -73,6 +73,7 @@ cd /tmp/nginx
|
|||||||
--with-file-aio \
|
--with-file-aio \
|
||||||
--with-http_sub_module \
|
--with-http_sub_module \
|
||||||
--with-http_ssl_module \
|
--with-http_ssl_module \
|
||||||
|
--with-http_v2_module \
|
||||||
--with-http_auth_request_module \
|
--with-http_auth_request_module \
|
||||||
--with-http_realip_module \
|
--with-http_realip_module \
|
||||||
--with-threads \
|
--with-threads \
|
||||||
|
|||||||
@ -105,9 +105,9 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
|
|||||||
# install legacy and standard intel icd and level-zero-gpu
|
# install legacy and standard intel icd and level-zero-gpu
|
||||||
# see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info
|
# see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info
|
||||||
# needed core package
|
# needed core package
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/libigdgmm12_22.5.5_amd64.deb
|
wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/libigdgmm12_22.7.0_amd64.deb
|
||||||
dpkg -i libigdgmm12_22.5.5_amd64.deb
|
dpkg -i libigdgmm12_22.7.0_amd64.deb
|
||||||
rm libigdgmm12_22.5.5_amd64.deb
|
rm libigdgmm12_22.7.0_amd64.deb
|
||||||
|
|
||||||
# legacy packages
|
# legacy packages
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb
|
wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb
|
||||||
@ -115,18 +115,19 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
|
|||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb
|
||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb
|
||||||
# standard packages
|
# standard packages
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-opencl-icd_24.52.32224.5_amd64.deb
|
wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/intel-opencl-icd_25.13.33276.19_amd64.deb
|
||||||
wget https://github.com/intel/compute-runtime/releases/download/24.52.32224.5/intel-level-zero-gpu_1.6.32224.5_amd64.deb
|
wget https://github.com/intel/compute-runtime/releases/download/25.13.33276.19/intel-level-zero-gpu_1.6.33276.19_amd64.deb
|
||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-opencl-2_2.5.6+18417_amd64.deb
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.10.10/intel-igc-opencl-2_2.10.10+18926_amd64.deb
|
||||||
wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.5.6/intel-igc-core-2_2.5.6+18417_amd64.deb
|
wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.10.10/intel-igc-core-2_2.10.10+18926_amd64.deb
|
||||||
# npu packages
|
# npu packages
|
||||||
wget https://github.com/oneapi-src/level-zero/releases/download/v1.21.9/level-zero_1.21.9+u22.04_amd64.deb
|
wget https://github.com/oneapi-src/level-zero/releases/download/v1.28.2/level-zero_1.28.2+u22.04_amd64.deb
|
||||||
wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-driver-compiler-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb
|
wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-driver-compiler-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb
|
||||||
wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-fw-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb
|
wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-fw-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb
|
||||||
wget https://github.com/intel/linux-npu-driver/releases/download/v1.17.0/intel-level-zero-npu_1.17.0.20250508-14912879441_ubuntu22.04_amd64.deb
|
wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-level-zero-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb
|
||||||
|
|
||||||
dpkg -i *.deb
|
dpkg -i *.deb
|
||||||
rm *.deb
|
rm *.deb
|
||||||
|
apt-get -qq install -f -y
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${TARGETARCH}" == "arm64" ]]; then
|
if [[ "${TARGETARCH}" == "arm64" ]]; then
|
||||||
|
|||||||
@ -63,6 +63,9 @@ http {
|
|||||||
server {
|
server {
|
||||||
include listen.conf;
|
include listen.conf;
|
||||||
|
|
||||||
|
# enable HTTP/2 for TLS connections to eliminate browser 6-connection limit
|
||||||
|
http2 on;
|
||||||
|
|
||||||
# vod settings
|
# vod settings
|
||||||
vod_base_url '';
|
vod_base_url '';
|
||||||
vod_segments_base_url '';
|
vod_segments_base_url '';
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
# NVidia TensorRT Support (amd64 only)
|
# Nvidia ONNX Runtime GPU Support
|
||||||
--extra-index-url 'https://pypi.nvidia.com'
|
--extra-index-url 'https://pypi.nvidia.com'
|
||||||
cython==3.0.*; platform_machine == 'x86_64'
|
cython==3.0.*; platform_machine == 'x86_64'
|
||||||
nvidia_cuda_cupti_cu12==12.5.82; platform_machine == 'x86_64'
|
nvidia-cuda-cupti-cu12==12.9.79; platform_machine == 'x86_64'
|
||||||
nvidia-cublas-cu12==12.5.3.*; platform_machine == 'x86_64'
|
nvidia-cublas-cu12==12.9.1.*; platform_machine == 'x86_64'
|
||||||
nvidia-cudnn-cu12==9.3.0.*; platform_machine == 'x86_64'
|
nvidia-cudnn-cu12==9.19.0.*; platform_machine == 'x86_64'
|
||||||
nvidia-cufft-cu12==11.2.3.*; platform_machine == 'x86_64'
|
nvidia-cufft-cu12==11.4.1.*; platform_machine == 'x86_64'
|
||||||
nvidia-curand-cu12==10.3.6.*; platform_machine == 'x86_64'
|
nvidia-curand-cu12==10.3.10.*; platform_machine == 'x86_64'
|
||||||
nvidia_cuda_nvcc_cu12==12.5.82; platform_machine == 'x86_64'
|
nvidia-cuda-nvcc-cu12==12.9.86; platform_machine == 'x86_64'
|
||||||
nvidia-cuda-nvrtc-cu12==12.5.82; platform_machine == 'x86_64'
|
nvidia-cuda-nvrtc-cu12==12.9.86; platform_machine == 'x86_64'
|
||||||
nvidia_cuda_runtime_cu12==12.5.82; platform_machine == 'x86_64'
|
nvidia-cuda-runtime-cu12==12.9.79; platform_machine == 'x86_64'
|
||||||
nvidia_cusolver_cu12==11.6.3.*; platform_machine == 'x86_64'
|
nvidia-cusolver-cu12==11.7.5.*; platform_machine == 'x86_64'
|
||||||
nvidia_cusparse_cu12==12.5.1.*; platform_machine == 'x86_64'
|
nvidia-cusparse-cu12==12.5.10.*; platform_machine == 'x86_64'
|
||||||
nvidia_nccl_cu12==2.23.4; platform_machine == 'x86_64'
|
nvidia-nccl-cu12==2.29.7; platform_machine == 'x86_64'
|
||||||
nvidia_nvjitlink_cu12==12.5.82; platform_machine == 'x86_64'
|
nvidia-nvjitlink-cu12==12.9.86; platform_machine == 'x86_64'
|
||||||
onnx==1.16.*; platform_machine == 'x86_64'
|
onnx==1.16.*; platform_machine == 'x86_64'
|
||||||
onnxruntime-gpu==1.22.*; platform_machine == 'x86_64'
|
onnxruntime-gpu==1.24.*; platform_machine == 'x86_64'
|
||||||
protobuf==3.20.3; platform_machine == 'x86_64'
|
protobuf==3.20.3; platform_machine == 'x86_64'
|
||||||
|
|||||||
@ -49,6 +49,11 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
|
||||||
|
|
||||||
|
**AXERA** <CommunityBadge />
|
||||||
|
|
||||||
|
- [AXEngine](#axera): axmodels can run on AXERA AI acceleration.
|
||||||
|
|
||||||
|
|
||||||
**For Testing**
|
**For Testing**
|
||||||
|
|
||||||
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
- [CPU Detector (not recommended for actual use](#cpu-detector-not-recommended): Use a CPU to run tflite model, this is not recommended and in most cases OpenVINO can be used in CPU mode with better results.
|
||||||
@ -1478,6 +1483,41 @@ model:
|
|||||||
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
input_pixel_format: rgb/bgr # look at the model.json to figure out which to put here
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## AXERA
|
||||||
|
|
||||||
|
Hardware accelerated object detection is supported on the following SoCs:
|
||||||
|
|
||||||
|
- AX650N
|
||||||
|
- AX8850N
|
||||||
|
|
||||||
|
This implementation uses the [AXera Pulsar2 Toolchain](https://huggingface.co/AXERA-TECH/Pulsar2).
|
||||||
|
|
||||||
|
See the [installation docs](../frigate/installation.md#axera) for information on configuring the AXEngine hardware.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
detectors:
|
||||||
|
axengine:
|
||||||
|
type: axengine
|
||||||
|
|
||||||
|
model:
|
||||||
|
path: frigate-yolov9-tiny
|
||||||
|
model_type: yolo-generic
|
||||||
|
width: 320
|
||||||
|
height: 320
|
||||||
|
tensor_format: bgr
|
||||||
|
labelmap_path: /labelmap/coco-80.txt
|
||||||
|
```
|
||||||
|
|
||||||
# Models
|
# Models
|
||||||
|
|
||||||
Some model types are not included in Frigate by default.
|
Some model types are not included in Frigate by default.
|
||||||
@ -1571,12 +1611,12 @@ YOLOv9 model can be exported as ONNX using the command below. You can copy and p
|
|||||||
```sh
|
```sh
|
||||||
docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
|
docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
|
||||||
FROM python:3.11 AS build
|
FROM python:3.11 AS build
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
|
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.8.0 /uv /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
|
||||||
WORKDIR /yolov9
|
WORKDIR /yolov9
|
||||||
ADD https://github.com/WongKinYiu/yolov9.git .
|
ADD https://github.com/WongKinYiu/yolov9.git .
|
||||||
RUN uv pip install --system -r requirements.txt
|
RUN uv pip install --system -r requirements.txt
|
||||||
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 onnxscript
|
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
|
||||||
ARG MODEL_SIZE
|
ARG MODEL_SIZE
|
||||||
ARG IMG_SIZE
|
ARG IMG_SIZE
|
||||||
ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
|
ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
|
||||||
|
|||||||
@ -76,6 +76,40 @@ Switching between V1 and V2 requires reindexing your embeddings. The embeddings
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### GenAI Provider
|
||||||
|
|
||||||
|
Frigate can use a GenAI provider for semantic search embeddings when that provider has the `embeddings` role. Currently, only **llama.cpp** supports multimodal embeddings (both text and images).
|
||||||
|
|
||||||
|
To use llama.cpp for semantic search:
|
||||||
|
|
||||||
|
1. Configure a GenAI provider in your config with `embeddings` in its `roles`.
|
||||||
|
2. Set `semantic_search.model` to the GenAI config key (e.g. `default`).
|
||||||
|
3. Start the llama.cpp server with `--embeddings` and `--mmproj` for image support:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
genai:
|
||||||
|
default:
|
||||||
|
provider: llamacpp
|
||||||
|
base_url: http://localhost:8080
|
||||||
|
model: your-model-name
|
||||||
|
roles:
|
||||||
|
- embeddings
|
||||||
|
- vision
|
||||||
|
- tools
|
||||||
|
|
||||||
|
semantic_search:
|
||||||
|
enabled: True
|
||||||
|
model: default
|
||||||
|
```
|
||||||
|
|
||||||
|
The llama.cpp server must be started with `--embeddings` for the embeddings API, and a multi-modal embeddings model. See the [llama.cpp server documentation](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) for details.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Switching between Jina models and a GenAI provider requires reindexing. Embeddings from different backends are incompatible.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
### GPU Acceleration
|
### GPU Acceleration
|
||||||
|
|
||||||
The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU hardware, when available. This depends on the Docker build that is used. You can also target a specific device in a multi-GPU installation.
|
The CLIP models are downloaded in ONNX format, and the `large` model can be accelerated using GPU hardware, when available. This depends on the Docker build that is used. You can also target a specific device in a multi-GPU installation.
|
||||||
|
|||||||
@ -103,6 +103,10 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection.
|
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs to provide efficient object detection.
|
||||||
|
|
||||||
|
**AXERA** <CommunityBadge />
|
||||||
|
|
||||||
|
- [AXEngine](#axera): axera models can run on AXERA NPUs via AXEngine, delivering highly efficient object detection.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
@ -196,13 +200,14 @@ Inference is done with the `onnx` detector type. Speeds will vary greatly depend
|
|||||||
✅ - Accelerated with CUDA Graphs
|
✅ - Accelerated with CUDA Graphs
|
||||||
❌ - Not accelerated with CUDA Graphs
|
❌ - Not accelerated with CUDA Graphs
|
||||||
|
|
||||||
| Name | ✅ YOLOv9 Inference Time | ✅ RF-DETR Inference Time | ❌ YOLO-NAS Inference Time |
|
| Name | ✅ YOLOv9 Inference Time | ✅ RF-DETR Inference Time | ❌ YOLO-NAS Inference Time |
|
||||||
| --------- | ------------------------------------- | ------------------------- | -------------------------- |
|
| ----------- | ------------------------------------- | ------------------------- | -------------------------- |
|
||||||
| GTX 1070 | s-320: 16 ms | | 320: 14 ms |
|
| GTX 1070 | s-320: 16 ms | | 320: 14 ms |
|
||||||
| RTX 3050 | t-320: 8 ms s-320: 10 ms s-640: 28 ms | Nano-320: ~ 12 ms | 320: ~ 10 ms 640: ~ 16 ms |
|
| RTX 3050 | t-320: 8 ms s-320: 10 ms s-640: 28 ms | Nano-320: ~ 12 ms | 320: ~ 10 ms 640: ~ 16 ms |
|
||||||
| RTX 3070 | t-320: 6 ms s-320: 8 ms s-640: 25 ms | Nano-320: ~ 9 ms | 320: ~ 8 ms 640: ~ 14 ms |
|
| RTX 3070 | t-320: 6 ms s-320: 8 ms s-640: 25 ms | Nano-320: ~ 9 ms | 320: ~ 8 ms 640: ~ 14 ms |
|
||||||
| RTX A4000 | | | 320: ~ 15 ms |
|
| RTX 5060 Ti | t-320: 5 ms s-320: 7 ms s-640: 22 ms | Nano-320: ~ 6 ms | |
|
||||||
| Tesla P40 | | | 320: ~ 105 ms |
|
| RTX A4000 | | | 320: ~ 15 ms |
|
||||||
|
| Tesla P40 | | | 320: ~ 105 ms |
|
||||||
|
|
||||||
### Apple Silicon
|
### Apple Silicon
|
||||||
|
|
||||||
@ -288,6 +293,14 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
|
|||||||
| ssd mobilenet | ~ 25 ms |
|
| ssd mobilenet | ~ 25 ms |
|
||||||
| yolov5m | ~ 118 ms |
|
| yolov5m | ~ 118 ms |
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
- **AXEngine** Default model is **yolov9**
|
||||||
|
|
||||||
|
| Name | AXERA AX650N/AX8850N Inference Time |
|
||||||
|
| ---------------- | ----------------------------------- |
|
||||||
|
| yolov9-tiny | ~ 4 ms |
|
||||||
|
|
||||||
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
|
||||||
|
|
||||||
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
||||||
|
|||||||
@ -439,6 +439,39 @@ or add these options to your `docker run` command:
|
|||||||
|
|
||||||
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
Next, you should configure [hardware object detection](/configuration/object_detectors#synaptics) and [hardware video processing](/configuration/hardware_acceleration_video#synaptics).
|
||||||
|
|
||||||
|
### AXERA
|
||||||
|
|
||||||
|
AXERA accelerators are available in an M.2 form factor, compatible with both Raspberry Pi and Orange Pi. This form factor has also been successfully tested on x86 platforms, making it a versatile choice for various computing environments.
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
Using AXERA accelerators requires the installation of the AXCL driver. We provide a convenient Linux script to complete this installation.
|
||||||
|
|
||||||
|
Follow these steps for installation:
|
||||||
|
|
||||||
|
1. Copy or download [this script](https://github.com/ivanshi1108/assets/releases/download/v0.16.2/user_installation.sh).
|
||||||
|
2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh`
|
||||||
|
3. Run the script with `./user_installation.sh`
|
||||||
|
|
||||||
|
#### Setup
|
||||||
|
|
||||||
|
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
|
||||||
|
|
||||||
|
Next, grant Docker permissions to access your hardware by adding the following lines to your `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
devices:
|
||||||
|
- /dev/axcl_host
|
||||||
|
- /dev/ax_mmb_dev
|
||||||
|
- /dev/msg_userdev
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using `docker run`, add this option to your command `--device /dev/axcl_host --device /dev/ax_mmb_dev --device /dev/msg_userdev`
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
Finally, configure [hardware object detection](/configuration/object_detectors#axera) to complete the setup.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Running through Docker with Docker Compose is the recommended install method.
|
Running through Docker with Docker Compose is the recommended install method.
|
||||||
|
|||||||
@ -159,7 +159,8 @@ Published when a license plate is recognized on a car object. See the [License P
|
|||||||
"plate": "123ABC",
|
"plate": "123ABC",
|
||||||
"score": 0.95,
|
"score": 0.95,
|
||||||
"camera": "driveway_cam",
|
"camera": "driveway_cam",
|
||||||
"timestamp": 1607123958.748393
|
"timestamp": 1607123958.748393,
|
||||||
|
"plate_box": [917, 487, 1029, 529] // box coordinates of the detected license plate in the frame
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -3,17 +3,67 @@ id: dummy-camera
|
|||||||
title: Analyzing Object Detection
|
title: Analyzing Object Detection
|
||||||
---
|
---
|
||||||
|
|
||||||
When investigating object detection or tracking problems, it can be helpful to replay an exported video as a temporary "dummy" camera. This lets you reproduce issues locally, iterate on configuration (detections, zones, enrichment settings), and capture logs and clips for analysis.
|
Frigate provides several tools for investigating object detection and tracking behavior: reviewing recorded detections through the UI, using the built-in Debug Replay feature, and manually setting up a dummy camera for advanced scenarios.
|
||||||
|
|
||||||
## When to use
|
## Reviewing Detections in the UI
|
||||||
|
|
||||||
- Replaying an exported clip to reproduce incorrect detections
|
Before setting up a replay, you can often diagnose detection issues by reviewing existing recordings directly in the Frigate UI.
|
||||||
- Testing configuration changes (model settings, trackers, filters) against a known clip
|
|
||||||
- Gathering deterministic logs and recordings for debugging or issue reports
|
|
||||||
|
|
||||||
## Example Config
|
### Detail View (History)
|
||||||
|
|
||||||
Place the clip you want to replay in a location accessible to Frigate (for example `/media/frigate/` or the repository `debug/` folder when developing). Then add a temporary camera to your `config/config.yml` like this:
|
The **Detail Stream** view in History shows recorded video with detection overlays (bounding boxes, path points, and zone highlights) drawn on top. Select a review item to see its tracked objects and lifecycle events. Clicking a lifecycle event seeks the video to that point so you can see exactly what the detector saw.
|
||||||
|
|
||||||
|
### Tracking Details (Explore)
|
||||||
|
|
||||||
|
In **Explore**, clicking a thumbnail opens the **Tracking Details** pane, which shows the full lifecycle of a single tracked object: every detection, zone entry/exit, and attribute change. The video plays back with the bounding box overlaid, letting you step through the object's entire lifecycle.
|
||||||
|
|
||||||
|
### Annotation Offset
|
||||||
|
|
||||||
|
Both views support an **Annotation Offset** setting (`detect.annotation_offset` in your camera config) that shifts the detection overlay in time relative to the recorded video. This compensates for the timing drift between the `detect` and `record` pipelines.
|
||||||
|
|
||||||
|
These streams use fundamentally different clocks with different buffering and latency characteristics, so the detection data and the recorded video are never perfectly synchronized. The annotation offset shifts the overlay to visually align the bounding boxes with the objects in the recorded video.
|
||||||
|
|
||||||
|
#### Why the offset varies between clips
|
||||||
|
|
||||||
|
The base timing drift between detect and record is roughly constant for a given camera, so a single offset value works well on average. However, you may notice the alignment is not pixel-perfect in every clip. This is normal and caused by several factors:
|
||||||
|
|
||||||
|
- **Keyframe-constrained seeking**: When the browser seeks to a timestamp, it can only land on the nearest keyframe. Each recording segment has keyframes at different positions relative to the detection timestamps, so the same offset may land slightly early in one clip and slightly late in another.
|
||||||
|
- **Segment boundary trimming**: When a recording range starts mid-segment, the video is trimmed to the requested start point. This trim may not align with a keyframe, shifting the effective reference point.
|
||||||
|
- **Capture-time jitter**: Network buffering, camera buffer flushes, and ffmpeg's own buffering mean the system-clock timestamp and the corresponding recorded frame are not always offset by exactly the same amount.
|
||||||
|
|
||||||
|
The per-clip variation is typically quite low and is mostly an artifact of keyframe granularity rather than a change in the true drift. A "perfect" alignment would require per-frame, keyframe-aware offset compensation, which is not practical. Treat the annotation offset as a best-effort average for your camera.
|
||||||
|
|
||||||
|
## Debug Replay
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### When to use
|
||||||
|
|
||||||
|
- Reproducing a detection or tracking issue from a specific time range
|
||||||
|
- Testing configuration changes (model settings, zones, filters, motion) against a known clip
|
||||||
|
- Gathering logs and debug overlays for a bug report
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
Only one replay session can be active at a time. If a session is already running, you will be prompted to navigate to it or stop it first.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Variables to consider
|
||||||
|
|
||||||
|
- The replay will not always produce identical results to the original run. Different frames may be selected on replay, which can change detections and tracking.
|
||||||
|
- Motion detection depends on the exact frames used; small frame shifts can change motion regions and therefore what gets passed to the detector.
|
||||||
|
- Object detection is not fully deterministic: models and post-processing can yield slightly different results across runs.
|
||||||
|
|
||||||
|
Treat the replay as a close approximation rather than an exact reproduction. Run multiple loops and examine the debug overlays and logs to understand the behavior.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### Example config
|
||||||
|
|
||||||
|
Place the clip you want to replay in a location accessible to Frigate (for example `/media/frigate/` or the repository `debug/` folder when developing). Then add a temporary camera to your `config/config.yml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
cameras:
|
cameras:
|
||||||
@ -32,10 +82,10 @@ cameras:
|
|||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
- `-re -stream_loop -1` tells `ffmpeg` to play the file in realtime and loop indefinitely, which is useful for long debugging sessions.
|
- `-re -stream_loop -1` tells ffmpeg to play the file in real time and loop indefinitely.
|
||||||
- `-fflags +genpts` helps generate presentation timestamps when they are missing in the file.
|
- `-fflags +genpts` generates presentation timestamps when they are missing in the file.
|
||||||
|
|
||||||
## Steps
|
### Steps
|
||||||
|
|
||||||
1. Export or copy the clip you want to replay to the Frigate host (e.g., `/media/frigate/` or `debug/clips/`). Depending on what you are looking to debug, it is often helpful to add some "pre-capture" time (where the tracked object is not yet visible) to the clip when exporting.
|
1. Export or copy the clip you want to replay to the Frigate host (e.g., `/media/frigate/` or `debug/clips/`). Depending on what you are looking to debug, it is often helpful to add some "pre-capture" time (where the tracked object is not yet visible) to the clip when exporting.
|
||||||
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.
|
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.
|
||||||
@ -45,16 +95,8 @@ cameras:
|
|||||||
5. Iterate on camera or enrichment settings (model, fps, zones, filters) and re-check the replay until the behavior is resolved.
|
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.
|
6. Remove the temporary camera from your config after debugging to avoid spurious telemetry or recordings.
|
||||||
|
|
||||||
## Variables to consider in object tracking
|
### Troubleshooting
|
||||||
|
|
||||||
- The exported video will not always line up exactly with how it originally ran through Frigate (or even with the last loop). Different frames may be used on replay, which can change detections and tracking.
|
- **No video**: verify the file path is correct and accessible from the Frigate process/container.
|
||||||
- Motion detection depends on the frames used; small frame shifts can change motion regions and therefore what gets passed to the detector.
|
- **FFmpeg errors**: check the log output and adjust `input_args` for your file format. You may also need to disable hardware acceleration (`hwaccel_args: ""`) for the dummy camera.
|
||||||
- Object detection is not deterministic: models and post-processing can yield different results across runs, so you may not get identical detections or track IDs every time.
|
- **No detections**: confirm the camera `roles` include `detect` and that the model/detector configuration is enabled.
|
||||||
|
|
||||||
When debugging, treat the replay as a close approximation rather than a byte-for-byte replay. Capture multiple runs, enable recording if helpful, and examine logs and saved event clips to understand variability.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- No video: verify the path is correct and accessible from the Frigate process/container.
|
|
||||||
- FFmpeg errors: check the log output for ffmpeg-specific flags and adjust `input_args` accordingly for your file/container. You may also need to disable hardware acceleration (`hwaccel_args: ""`) for the dummy camera.
|
|
||||||
- No detections: confirm the camera `roles` include `detect`, and model/detector configuration is enabled.
|
|
||||||
|
|||||||
@ -589,23 +589,38 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
request.app.frigate_config = config
|
request.app.frigate_config = config
|
||||||
request.app.genai_manager.update_config(config)
|
request.app.genai_manager.update_config(config)
|
||||||
|
|
||||||
|
if request.app.stats_emitter is not None:
|
||||||
|
request.app.stats_emitter.config = config
|
||||||
|
|
||||||
if body.update_topic:
|
if body.update_topic:
|
||||||
if body.update_topic.startswith("config/cameras/"):
|
if body.update_topic.startswith("config/cameras/"):
|
||||||
_, _, camera, field = body.update_topic.split("/")
|
_, _, camera, field = body.update_topic.split("/")
|
||||||
|
|
||||||
if field == "add":
|
if camera == "*":
|
||||||
settings = config.cameras[camera]
|
# Wildcard: fan out update to all cameras
|
||||||
elif field == "remove":
|
enum_value = CameraConfigUpdateEnum[field]
|
||||||
settings = old_config.cameras[camera]
|
for camera_name in config.cameras:
|
||||||
|
settings = config.get_nested_object(
|
||||||
|
f"config/cameras/{camera_name}/{field}"
|
||||||
|
)
|
||||||
|
request.app.config_publisher.publish_update(
|
||||||
|
CameraConfigUpdateTopic(enum_value, camera_name),
|
||||||
|
settings,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
settings = config.get_nested_object(body.update_topic)
|
if field == "add":
|
||||||
|
settings = config.cameras[camera]
|
||||||
|
elif field == "remove":
|
||||||
|
settings = old_config.cameras[camera]
|
||||||
|
else:
|
||||||
|
settings = config.get_nested_object(body.update_topic)
|
||||||
|
|
||||||
request.app.config_publisher.publish_update(
|
request.app.config_publisher.publish_update(
|
||||||
CameraConfigUpdateTopic(
|
CameraConfigUpdateTopic(
|
||||||
CameraConfigUpdateEnum[field], camera
|
CameraConfigUpdateEnum[field], camera
|
||||||
),
|
),
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Generic handling for global config updates
|
# Generic handling for global config updates
|
||||||
settings = config.get_nested_object(body.update_topic)
|
settings = config.get_nested_object(body.update_topic)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Camera apis."""
|
"""Camera apis."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -11,7 +12,9 @@ import httpx
|
|||||||
import requests
|
import requests
|
||||||
from fastapi import APIRouter, Depends, Query, Request, Response
|
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from filelock import FileLock, Timeout
|
||||||
from onvif import ONVIFCamera, ONVIFError
|
from onvif import ONVIFCamera, ONVIFError
|
||||||
|
from ruamel.yaml import YAML
|
||||||
from zeep.exceptions import Fault, TransportError
|
from zeep.exceptions import Fault, TransportError
|
||||||
from zeep.transports import AsyncTransport
|
from zeep.transports import AsyncTransport
|
||||||
|
|
||||||
@ -21,8 +24,14 @@ from frigate.api.auth import (
|
|||||||
require_role,
|
require_role,
|
||||||
)
|
)
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.config.camera.updater import (
|
||||||
|
CameraConfigUpdateEnum,
|
||||||
|
CameraConfigUpdateTopic,
|
||||||
|
)
|
||||||
from frigate.util.builtin import clean_camera_user_pass
|
from frigate.util.builtin import clean_camera_user_pass
|
||||||
|
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.image import run_ffmpeg_snapshot
|
||||||
from frigate.util.services import ffprobe_stream
|
from frigate.util.services import ffprobe_stream
|
||||||
|
|
||||||
@ -995,3 +1004,154 @@ async def onvif_probe(
|
|||||||
await onvif_camera.close()
|
await onvif_camera.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error closing ONVIF camera session: {e}")
|
logger.debug(f"Error closing ONVIF camera session: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/cameras/{camera_name}",
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
async def delete_camera(
|
||||||
|
request: Request,
|
||||||
|
camera_name: str,
|
||||||
|
delete_exports: bool = Query(default=False),
|
||||||
|
):
|
||||||
|
"""Delete a camera and all its associated data.
|
||||||
|
|
||||||
|
Removes the camera from config, stops processes, and cleans up
|
||||||
|
all database entries and media files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_name: Name of the camera to delete
|
||||||
|
delete_exports: Whether to also delete exports for this camera
|
||||||
|
"""
|
||||||
|
frigate_config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
|
if camera_name not in frigate_config.cameras:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": f"Camera {camera_name} not found",
|
||||||
|
},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
old_camera_config = frigate_config.cameras[camera_name]
|
||||||
|
config_file = find_config_file()
|
||||||
|
lock = FileLock(f"{config_file}.lock", timeout=5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock:
|
||||||
|
with open(config_file, "r") 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:
|
||||||
|
data = yaml.load(f)
|
||||||
|
|
||||||
|
# Remove camera from config
|
||||||
|
if "cameras" in data and camera_name in data["cameras"]:
|
||||||
|
del data["cameras"][camera_name]
|
||||||
|
|
||||||
|
# Remove camera from auth roles
|
||||||
|
auth = data.get("auth", {})
|
||||||
|
if auth and "roles" in auth:
|
||||||
|
empty_roles = []
|
||||||
|
for role_name, cameras_list in auth["roles"].items():
|
||||||
|
if (
|
||||||
|
isinstance(cameras_list, list)
|
||||||
|
and camera_name in cameras_list
|
||||||
|
):
|
||||||
|
cameras_list.remove(camera_name)
|
||||||
|
# Custom roles can't be empty; mark for removal
|
||||||
|
if not cameras_list and role_name not in (
|
||||||
|
"admin",
|
||||||
|
"viewer",
|
||||||
|
):
|
||||||
|
empty_roles.append(role_name)
|
||||||
|
for role_name in empty_roles:
|
||||||
|
del auth["roles"][role_name]
|
||||||
|
|
||||||
|
with open(config_file, "w") as f:
|
||||||
|
yaml.dump(data, f)
|
||||||
|
|
||||||
|
with open(config_file, "r") as f:
|
||||||
|
new_raw_config = f.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = FrigateConfig.parse(new_raw_config)
|
||||||
|
except Exception:
|
||||||
|
with open(config_file, "w") as f:
|
||||||
|
f.write(old_raw_config)
|
||||||
|
logger.exception(
|
||||||
|
"Config error after removing camera %s",
|
||||||
|
camera_name,
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "Error parsing config after camera removal",
|
||||||
|
},
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Error updating config to remove camera %s: %s", camera_name, e
|
||||||
|
)
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "Error updating config",
|
||||||
|
},
|
||||||
|
status_code=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update runtime config
|
||||||
|
request.app.frigate_config = config
|
||||||
|
request.app.genai_manager.update_config(config)
|
||||||
|
|
||||||
|
# Publish removal to stop ffmpeg processes and clean up runtime state
|
||||||
|
request.app.config_publisher.publish_update(
|
||||||
|
CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, camera_name),
|
||||||
|
old_camera_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Timeout:
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": False,
|
||||||
|
"message": "Another process is currently updating the config",
|
||||||
|
},
|
||||||
|
status_code=409,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up database entries
|
||||||
|
counts, export_paths = await asyncio.to_thread(
|
||||||
|
cleanup_camera_db, camera_name, delete_exports
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up media files in background thread
|
||||||
|
await asyncio.to_thread(
|
||||||
|
cleanup_camera_files, camera_name, export_paths if delete_exports else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Best-effort go2rtc stream removal
|
||||||
|
try:
|
||||||
|
requests.delete(
|
||||||
|
"http://127.0.0.1:1984/api/streams",
|
||||||
|
params={"src": camera_name},
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to remove go2rtc stream for %s", camera_name)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
content={
|
||||||
|
"success": True,
|
||||||
|
"message": f"Camera {camera_name} has been deleted",
|
||||||
|
"cleanup": counts,
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Event apis."""
|
"""Event apis."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
@ -1124,7 +1125,9 @@ async def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = N
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
plus_id = request.app.frigate_config.plus_api.upload_image(image, event.camera)
|
plus_id = await asyncio.to_thread(
|
||||||
|
request.app.frigate_config.plus_api.upload_image, image, event.camera
|
||||||
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.exception(ex)
|
logger.exception(ex)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1140,7 +1143,8 @@ async def send_to_plus(request: Request, event_id: str, body: SubmitPlusBody = N
|
|||||||
box = event.data["box"]
|
box = event.data["box"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request.app.frigate_config.plus_api.add_annotation(
|
await asyncio.to_thread(
|
||||||
|
request.app.frigate_config.plus_api.add_annotation,
|
||||||
event.plus_id,
|
event.plus_id,
|
||||||
box,
|
box,
|
||||||
event.label,
|
event.label,
|
||||||
@ -1230,7 +1234,8 @@ async def false_positive(request: Request, event_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request.app.frigate_config.plus_api.add_false_positive(
|
await asyncio.to_thread(
|
||||||
|
request.app.frigate_config.plus_api.add_false_positive,
|
||||||
event.plus_id,
|
event.plus_id,
|
||||||
region,
|
region,
|
||||||
box,
|
box,
|
||||||
|
|||||||
@ -1281,6 +1281,13 @@ def preview_gif(
|
|||||||
else:
|
else:
|
||||||
# need to generate from existing images
|
# need to generate from existing images
|
||||||
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
||||||
|
|
||||||
|
if not os.path.isdir(preview_dir):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Preview not found"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
file_start = f"preview_{camera_name}"
|
file_start = f"preview_{camera_name}"
|
||||||
start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}"
|
start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}"
|
||||||
end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}"
|
end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}"
|
||||||
@ -1456,6 +1463,13 @@ def preview_mp4(
|
|||||||
else:
|
else:
|
||||||
# need to generate from existing images
|
# need to generate from existing images
|
||||||
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
preview_dir = os.path.join(CACHE_DIR, "preview_frames")
|
||||||
|
|
||||||
|
if not os.path.isdir(preview_dir):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Preview not found"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
file_start = f"preview_{camera_name}"
|
file_start = f"preview_{camera_name}"
|
||||||
start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}"
|
start_file = f"{file_start}-{start_ts}.{PREVIEW_FRAME_TYPE}"
|
||||||
end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}"
|
end_file = f"{file_start}-{end_ts}.{PREVIEW_FRAME_TYPE}"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Recording APIs."""
|
"""Recording APIs."""
|
||||||
|
|
||||||
|
import datetime as dt
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
@ -190,45 +191,22 @@ def all_recordings_summary(
|
|||||||
days: dict[str, bool] = {}
|
days: dict[str, bool] = {}
|
||||||
|
|
||||||
for period_start, period_end, period_offset in dst_periods:
|
for period_start, period_end, period_offset in dst_periods:
|
||||||
hours_offset = int(period_offset / 60 / 60)
|
day_expr = ((Recordings.start_time + period_offset) / 86400).cast("int")
|
||||||
minutes_offset = int(period_offset / 60 - hours_offset * 60)
|
|
||||||
period_hour_modifier = f"{hours_offset} hour"
|
|
||||||
period_minute_modifier = f"{minutes_offset} minute"
|
|
||||||
|
|
||||||
period_query = (
|
period_query = (
|
||||||
Recordings.select(
|
Recordings.select(day_expr.alias("day_idx"))
|
||||||
fn.strftime(
|
|
||||||
"%Y-%m-%d",
|
|
||||||
fn.datetime(
|
|
||||||
Recordings.start_time,
|
|
||||||
"unixepoch",
|
|
||||||
period_hour_modifier,
|
|
||||||
period_minute_modifier,
|
|
||||||
),
|
|
||||||
).alias("day")
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(Recordings.camera << camera_list)
|
(Recordings.camera << camera_list)
|
||||||
& (Recordings.end_time >= period_start)
|
& (Recordings.end_time >= period_start)
|
||||||
& (Recordings.start_time <= period_end)
|
& (Recordings.start_time <= period_end)
|
||||||
)
|
)
|
||||||
.group_by(
|
.distinct()
|
||||||
fn.strftime(
|
|
||||||
"%Y-%m-%d",
|
|
||||||
fn.datetime(
|
|
||||||
Recordings.start_time,
|
|
||||||
"unixepoch",
|
|
||||||
period_hour_modifier,
|
|
||||||
period_minute_modifier,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.order_by(Recordings.start_time.desc())
|
|
||||||
.namedtuples()
|
.namedtuples()
|
||||||
)
|
)
|
||||||
|
|
||||||
for g in period_query:
|
for g in period_query:
|
||||||
days[g.day] = True
|
day_str = (dt.date(1970, 1, 1) + dt.timedelta(days=g.day_idx)).isoformat()
|
||||||
|
days[day_str] = True
|
||||||
|
|
||||||
return JSONResponse(content=dict(sorted(days.items())))
|
return JSONResponse(content=dict(sorted(days.items())))
|
||||||
|
|
||||||
|
|||||||
@ -256,6 +256,14 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
def create_ffmpeg_cmds(self):
|
def create_ffmpeg_cmds(self):
|
||||||
if "_ffmpeg_cmds" in self:
|
if "_ffmpeg_cmds" in self:
|
||||||
return
|
return
|
||||||
|
self._build_ffmpeg_cmds()
|
||||||
|
|
||||||
|
def recreate_ffmpeg_cmds(self):
|
||||||
|
"""Force regeneration of ffmpeg commands from current config."""
|
||||||
|
self._build_ffmpeg_cmds()
|
||||||
|
|
||||||
|
def _build_ffmpeg_cmds(self):
|
||||||
|
"""Build ffmpeg commands from the current ffmpeg config."""
|
||||||
ffmpeg_cmds = []
|
ffmpeg_cmds = []
|
||||||
for ffmpeg_input in self.ffmpeg.inputs:
|
for ffmpeg_input in self.ffmpeg.inputs:
|
||||||
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
|
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ class CameraConfigUpdateEnum(str, Enum):
|
|||||||
birdseye = "birdseye"
|
birdseye = "birdseye"
|
||||||
detect = "detect"
|
detect = "detect"
|
||||||
enabled = "enabled"
|
enabled = "enabled"
|
||||||
|
ffmpeg = "ffmpeg"
|
||||||
motion = "motion" # includes motion and motion masks
|
motion = "motion" # includes motion and motion masks
|
||||||
notifications = "notifications"
|
notifications = "notifications"
|
||||||
objects = "objects"
|
objects = "objects"
|
||||||
@ -91,6 +92,9 @@ class CameraConfigUpdateSubscriber:
|
|||||||
|
|
||||||
if update_type == CameraConfigUpdateEnum.audio:
|
if update_type == CameraConfigUpdateEnum.audio:
|
||||||
config.audio = updated_config
|
config.audio = updated_config
|
||||||
|
elif update_type == CameraConfigUpdateEnum.ffmpeg:
|
||||||
|
config.ffmpeg = updated_config
|
||||||
|
config.recreate_ffmpeg_cmds()
|
||||||
elif update_type == CameraConfigUpdateEnum.audio_transcription:
|
elif update_type == CameraConfigUpdateEnum.audio_transcription:
|
||||||
config.audio_transcription = updated_config
|
config.audio_transcription = updated_config
|
||||||
elif update_type == CameraConfigUpdateEnum.birdseye:
|
elif update_type == CameraConfigUpdateEnum.birdseye:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
|
|
||||||
@ -173,10 +173,10 @@ class SemanticSearchConfig(FrigateBaseModel):
|
|||||||
title="Reindex on startup",
|
title="Reindex on startup",
|
||||||
description="Trigger a full reindex of historical tracked objects into the embeddings database.",
|
description="Trigger a full reindex of historical tracked objects into the embeddings database.",
|
||||||
)
|
)
|
||||||
model: Optional[SemanticSearchModelEnum] = Field(
|
model: Optional[Union[SemanticSearchModelEnum, str]] = Field(
|
||||||
default=SemanticSearchModelEnum.jinav1,
|
default=SemanticSearchModelEnum.jinav1,
|
||||||
title="Semantic search model",
|
title="Semantic search model or GenAI provider name",
|
||||||
description="The embeddings model to use for semantic search (for example 'jinav1').",
|
description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.",
|
||||||
)
|
)
|
||||||
model_size: str = Field(
|
model_size: str = Field(
|
||||||
default="small",
|
default="small",
|
||||||
|
|||||||
@ -61,6 +61,7 @@ from .classification import (
|
|||||||
FaceRecognitionConfig,
|
FaceRecognitionConfig,
|
||||||
LicensePlateRecognitionConfig,
|
LicensePlateRecognitionConfig,
|
||||||
SemanticSearchConfig,
|
SemanticSearchConfig,
|
||||||
|
SemanticSearchModelEnum,
|
||||||
)
|
)
|
||||||
from .database import DatabaseConfig
|
from .database import DatabaseConfig
|
||||||
from .env import EnvVars
|
from .env import EnvVars
|
||||||
@ -592,6 +593,24 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
role_to_name[role] = name
|
role_to_name[role] = name
|
||||||
|
|
||||||
|
# validate semantic_search.model when it is a GenAI provider name
|
||||||
|
if (
|
||||||
|
self.semantic_search.enabled
|
||||||
|
and isinstance(self.semantic_search.model, str)
|
||||||
|
and not isinstance(self.semantic_search.model, SemanticSearchModelEnum)
|
||||||
|
):
|
||||||
|
if self.semantic_search.model not in self.genai:
|
||||||
|
raise ValueError(
|
||||||
|
f"semantic_search.model '{self.semantic_search.model}' is not a "
|
||||||
|
"valid GenAI config key. Must match a key in genai config."
|
||||||
|
)
|
||||||
|
genai_cfg = self.genai[self.semantic_search.model]
|
||||||
|
if GenAIRoleEnum.embeddings not in genai_cfg.roles:
|
||||||
|
raise ValueError(
|
||||||
|
f"GenAI provider '{self.semantic_search.model}' must have "
|
||||||
|
"'embeddings' in its roles for semantic search."
|
||||||
|
)
|
||||||
|
|
||||||
# set default min_score for object attributes
|
# set default min_score for object attributes
|
||||||
for attribute in self.model.all_attributes:
|
for attribute in self.model.all_attributes:
|
||||||
if not self.objects.filters.get(attribute):
|
if not self.objects.filters.get(attribute):
|
||||||
|
|||||||
@ -401,35 +401,10 @@ class LicensePlateProcessingMixin:
|
|||||||
all_confidences.append(flat_confidences)
|
all_confidences.append(flat_confidences)
|
||||||
all_areas.append(combined_area)
|
all_areas.append(combined_area)
|
||||||
|
|
||||||
# Step 3: Filter and sort the combined plates
|
# Step 3: Sort the combined plates
|
||||||
if all_license_plates:
|
if all_license_plates:
|
||||||
filtered_data = []
|
|
||||||
for plate, conf_list, area in zip(
|
|
||||||
all_license_plates, all_confidences, all_areas
|
|
||||||
):
|
|
||||||
if len(plate) < self.lpr_config.min_plate_length:
|
|
||||||
logger.debug(
|
|
||||||
f"{camera}: Filtered out '{plate}' due to length ({len(plate)} < {self.lpr_config.min_plate_length})"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.lpr_config.format:
|
|
||||||
try:
|
|
||||||
if not re.fullmatch(self.lpr_config.format, plate):
|
|
||||||
logger.debug(
|
|
||||||
f"{camera}: Filtered out '{plate}' due to format mismatch"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
except re.error:
|
|
||||||
# Skip format filtering if regex is invalid
|
|
||||||
logger.error(
|
|
||||||
f"{camera}: Invalid regex in LPR format configuration: {self.lpr_config.format}"
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered_data.append((plate, conf_list, area))
|
|
||||||
|
|
||||||
sorted_data = sorted(
|
sorted_data = sorted(
|
||||||
filtered_data,
|
zip(all_license_plates, all_confidences, all_areas),
|
||||||
key=lambda x: (x[2], len(x[0]), sum(x[1]) / len(x[1]) if x[1] else 0),
|
key=lambda x: (x[2], len(x[0]), sum(x[1]) / len(x[1]) if x[1] else 0),
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
@ -1250,6 +1225,8 @@ class LicensePlateProcessingMixin:
|
|||||||
logger.debug(f"{camera}: License plate area below minimum threshold.")
|
logger.debug(f"{camera}: License plate area below minimum threshold.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
plate_box = license_plate
|
||||||
|
|
||||||
license_plate_frame = rgb[
|
license_plate_frame = rgb[
|
||||||
license_plate[1] : license_plate[3],
|
license_plate[1] : license_plate[3],
|
||||||
license_plate[0] : license_plate[2],
|
license_plate[0] : license_plate[2],
|
||||||
@ -1366,6 +1343,20 @@ class LicensePlateProcessingMixin:
|
|||||||
logger.debug(f"{camera}: License plate is less than min_area")
|
logger.debug(f"{camera}: License plate is less than min_area")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Scale back to original car coordinates and then to frame
|
||||||
|
plate_box_in_car = (
|
||||||
|
license_plate[0] // 2,
|
||||||
|
license_plate[1] // 2,
|
||||||
|
license_plate[2] // 2,
|
||||||
|
license_plate[3] // 2,
|
||||||
|
)
|
||||||
|
plate_box = (
|
||||||
|
left + plate_box_in_car[0],
|
||||||
|
top + plate_box_in_car[1],
|
||||||
|
left + plate_box_in_car[2],
|
||||||
|
top + plate_box_in_car[3],
|
||||||
|
)
|
||||||
|
|
||||||
license_plate_frame = car[
|
license_plate_frame = car[
|
||||||
license_plate[1] : license_plate[3],
|
license_plate[1] : license_plate[3],
|
||||||
license_plate[0] : license_plate[2],
|
license_plate[0] : license_plate[2],
|
||||||
@ -1429,6 +1420,8 @@ class LicensePlateProcessingMixin:
|
|||||||
0, [license_plate_frame.shape[1], license_plate_frame.shape[0]] * 2
|
0, [license_plate_frame.shape[1], license_plate_frame.shape[0]] * 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
plate_box = tuple(int(x) for x in expanded_box)
|
||||||
|
|
||||||
# Crop using the expanded box
|
# Crop using the expanded box
|
||||||
license_plate_frame = license_plate_frame[
|
license_plate_frame = license_plate_frame[
|
||||||
int(expanded_box[1]) : int(expanded_box[3]),
|
int(expanded_box[1]) : int(expanded_box[3]),
|
||||||
@ -1557,6 +1550,27 @@ class LicensePlateProcessingMixin:
|
|||||||
f"{camera}: Clustering changed top plate '{top_plate}' (conf: {avg_confidence:.3f}) to rep '{rep_plate}' (conf: {rep_conf:.3f})"
|
f"{camera}: Clustering changed top plate '{top_plate}' (conf: {avg_confidence:.3f}) to rep '{rep_plate}' (conf: {rep_conf:.3f})"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Apply length and format filters to the clustered representative
|
||||||
|
# rather than individual OCR readings, so noisy variants still
|
||||||
|
# contribute to clustering even when they don't pass on their own.
|
||||||
|
if len(rep_plate) < self.lpr_config.min_plate_length:
|
||||||
|
logger.debug(
|
||||||
|
f"{camera}: Filtered out clustered plate '{rep_plate}' due to length ({len(rep_plate)} < {self.lpr_config.min_plate_length})"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.lpr_config.format:
|
||||||
|
try:
|
||||||
|
if not re.fullmatch(self.lpr_config.format, rep_plate):
|
||||||
|
logger.debug(
|
||||||
|
f"{camera}: Filtered out clustered plate '{rep_plate}' due to format mismatch"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except re.error:
|
||||||
|
logger.error(
|
||||||
|
f"{camera}: Invalid regex in LPR format configuration: {self.lpr_config.format}"
|
||||||
|
)
|
||||||
|
|
||||||
# Update stored rep
|
# Update stored rep
|
||||||
self.detected_license_plates[id].update(
|
self.detected_license_plates[id].update(
|
||||||
{
|
{
|
||||||
@ -1615,6 +1629,7 @@ class LicensePlateProcessingMixin:
|
|||||||
"id": id,
|
"id": id,
|
||||||
"camera": camera,
|
"camera": camera,
|
||||||
"timestamp": start,
|
"timestamp": start,
|
||||||
|
"plate_box": plate_box,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -50,3 +50,16 @@ class PostProcessorApi(ABC):
|
|||||||
None if request was not handled, otherwise return response.
|
None if request was not handled, otherwise return response.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Handle a config change notification.
|
||||||
|
|
||||||
|
Called for every config update published under ``config/``.
|
||||||
|
Processors should override this to check the topic and act only
|
||||||
|
on changes relevant to them. Default is a no-op.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: The config topic that changed.
|
||||||
|
payload: The updated configuration object.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@ -47,6 +47,16 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
|
|||||||
self.sub_label_publisher = sub_label_publisher
|
self.sub_label_publisher = sub_label_publisher
|
||||||
super().__init__(config, metrics, model_runner)
|
super().__init__(config, metrics, model_runner)
|
||||||
|
|
||||||
|
CONFIG_UPDATE_TOPIC = "config/lpr"
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Update LPR config at runtime."""
|
||||||
|
if topic != self.CONFIG_UPDATE_TOPIC:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.lpr_config = payload
|
||||||
|
logger.debug("LPR post-processor config updated dynamically")
|
||||||
|
|
||||||
def process_data(
|
def process_data(
|
||||||
self, data: dict[str, Any], data_type: PostProcessDataEnum
|
self, data: dict[str, Any], data_type: PostProcessDataEnum
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@ -4,20 +4,23 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||||||
class ReviewMetadata(BaseModel):
|
class ReviewMetadata(BaseModel):
|
||||||
model_config = ConfigDict(extra="ignore", protected_namespaces=())
|
model_config = ConfigDict(extra="ignore", protected_namespaces=())
|
||||||
|
|
||||||
title: str = Field(description="A concise title for the activity.")
|
title: str = Field(
|
||||||
|
description="A short title characterizing what took place and where, under 10 words."
|
||||||
|
)
|
||||||
scene: str = Field(
|
scene: str = Field(
|
||||||
description="A comprehensive description of the setting and entities, including relevant context and plausible inferences if supported by visual evidence."
|
description="A chronological narrative of what happens from start to finish."
|
||||||
)
|
)
|
||||||
shortSummary: str = Field(
|
shortSummary: str = Field(
|
||||||
description="A brief 2-sentence summary of the scene, suitable for notifications. Should capture the key activity and context without full detail."
|
description="A brief 2-sentence summary of the scene, suitable for notifications."
|
||||||
)
|
)
|
||||||
confidence: float = Field(
|
confidence: float = Field(
|
||||||
description="A float between 0 and 1 representing your overall confidence in this analysis."
|
ge=0.0,
|
||||||
|
description="Confidence in the analysis, from 0 to 1.",
|
||||||
)
|
)
|
||||||
potential_threat_level: int = Field(
|
potential_threat_level: int = Field(
|
||||||
ge=0,
|
ge=0,
|
||||||
le=3,
|
le=2,
|
||||||
description="An integer representing the potential threat level (1-3). 1: Minor anomaly. 2: Moderate concern. 3: High threat. Only include this field if a clear security concern is observable; otherwise, omit it.",
|
description="Threat level: 0 = normal, 1 = suspicious, 2 = critical threat.",
|
||||||
)
|
)
|
||||||
other_concerns: list[str] | None = Field(
|
other_concerns: list[str] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
|
|||||||
@ -61,3 +61,16 @@ class RealTimeProcessorApi(ABC):
|
|||||||
None.
|
None.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Handle a config change notification.
|
||||||
|
|
||||||
|
Called for every config update published under ``config/``.
|
||||||
|
Processors should override this to check the topic and act only
|
||||||
|
on changes relevant to them. Default is a no-op.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: The config topic that changed.
|
||||||
|
payload: The updated configuration object.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@ -169,6 +169,16 @@ class BirdRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
)
|
)
|
||||||
self.detected_birds[obj_data["id"]] = score
|
self.detected_birds[obj_data["id"]] = score
|
||||||
|
|
||||||
|
CONFIG_UPDATE_TOPIC = "config/classification"
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Update bird classification config at runtime."""
|
||||||
|
if topic != self.CONFIG_UPDATE_TOPIC:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.config.classification = payload
|
||||||
|
logger.debug("Bird classification config updated dynamically")
|
||||||
|
|
||||||
def handle_request(self, topic, request_data):
|
def handle_request(self, topic, request_data):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@ -95,6 +95,23 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
|
|
||||||
self.recognizer.build()
|
self.recognizer.build()
|
||||||
|
|
||||||
|
CONFIG_UPDATE_TOPIC = "config/face_recognition"
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Update face recognition config at runtime."""
|
||||||
|
if topic != self.CONFIG_UPDATE_TOPIC:
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_min_area = self.config.face_recognition.min_area
|
||||||
|
self.config.face_recognition = payload
|
||||||
|
self.face_config = payload
|
||||||
|
|
||||||
|
for camera_config in self.config.cameras.values():
|
||||||
|
if camera_config.face_recognition.min_area == previous_min_area:
|
||||||
|
camera_config.face_recognition.min_area = payload.min_area
|
||||||
|
|
||||||
|
logger.debug("Face recognition config updated dynamically")
|
||||||
|
|
||||||
def __download_models(self, path: str) -> None:
|
def __download_models(self, path: str) -> None:
|
||||||
try:
|
try:
|
||||||
file_name = os.path.basename(path)
|
file_name = os.path.basename(path)
|
||||||
|
|||||||
@ -40,6 +40,23 @@ class LicensePlateRealTimeProcessor(LicensePlateProcessingMixin, RealTimeProcess
|
|||||||
self.camera_current_cars: dict[str, list[str]] = {}
|
self.camera_current_cars: dict[str, list[str]] = {}
|
||||||
super().__init__(config, metrics)
|
super().__init__(config, metrics)
|
||||||
|
|
||||||
|
CONFIG_UPDATE_TOPIC = "config/lpr"
|
||||||
|
|
||||||
|
def update_config(self, topic: str, payload: Any) -> None:
|
||||||
|
"""Update LPR config at runtime."""
|
||||||
|
if topic != self.CONFIG_UPDATE_TOPIC:
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_min_area = self.config.lpr.min_area
|
||||||
|
self.config.lpr = payload
|
||||||
|
self.lpr_config = payload
|
||||||
|
|
||||||
|
for camera_config in self.config.cameras.values():
|
||||||
|
if camera_config.lpr.min_area == previous_min_area:
|
||||||
|
camera_config.lpr.min_area = payload.min_area
|
||||||
|
|
||||||
|
logger.debug("LPR config updated dynamically")
|
||||||
|
|
||||||
def process_frame(
|
def process_frame(
|
||||||
self,
|
self,
|
||||||
obj_data: dict[str, Any],
|
obj_data: dict[str, Any],
|
||||||
|
|||||||
@ -21,7 +21,8 @@ from frigate.const import (
|
|||||||
REPLAY_DIR,
|
REPLAY_DIR,
|
||||||
THUMB_DIR,
|
THUMB_DIR,
|
||||||
)
|
)
|
||||||
from frigate.models import Event, Recordings, ReviewSegment, Timeline
|
from frigate.models import Recordings
|
||||||
|
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
|
||||||
from frigate.util.config import find_config_file
|
from frigate.util.config import find_config_file
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -357,43 +358,13 @@ class DebugReplayManager:
|
|||||||
|
|
||||||
def _cleanup_db(self, camera_name: str) -> None:
|
def _cleanup_db(self, camera_name: str) -> None:
|
||||||
"""Defensively remove any database rows for the replay camera."""
|
"""Defensively remove any database rows for the replay camera."""
|
||||||
try:
|
cleanup_camera_db(camera_name)
|
||||||
Event.delete().where(Event.camera == camera_name).execute()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to delete replay events: %s", e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
Timeline.delete().where(Timeline.camera == camera_name).execute()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to delete replay timeline: %s", e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
Recordings.delete().where(Recordings.camera == camera_name).execute()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to delete replay recordings: %s", e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
ReviewSegment.delete().where(ReviewSegment.camera == camera_name).execute()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to delete replay review segments: %s", e)
|
|
||||||
|
|
||||||
def _cleanup_files(self, camera_name: str) -> None:
|
def _cleanup_files(self, camera_name: str) -> None:
|
||||||
"""Remove filesystem artifacts for the replay camera."""
|
"""Remove filesystem artifacts for the replay camera."""
|
||||||
dirs_to_clean = [
|
cleanup_camera_files(camera_name)
|
||||||
os.path.join(RECORD_DIR, camera_name),
|
|
||||||
os.path.join(CLIPS_DIR, camera_name),
|
|
||||||
os.path.join(THUMB_DIR, camera_name),
|
|
||||||
]
|
|
||||||
|
|
||||||
for dir_path in dirs_to_clean:
|
# Remove replay-specific cache directory
|
||||||
if os.path.exists(dir_path):
|
|
||||||
try:
|
|
||||||
shutil.rmtree(dir_path)
|
|
||||||
logger.debug("Removed replay directory: %s", dir_path)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to remove %s: %s", dir_path, e)
|
|
||||||
|
|
||||||
# Remove replay clip and any related files
|
|
||||||
if os.path.exists(REPLAY_DIR):
|
if os.path.exists(REPLAY_DIR):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(REPLAY_DIR)
|
shutil.rmtree(REPLAY_DIR)
|
||||||
|
|||||||
86
frigate/detectors/plugins/axengine.py
Normal file
86
frigate/detectors/plugins/axengine.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import axengine as axe
|
||||||
|
|
||||||
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
|
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||||
|
from frigate.util.model import post_process_yolo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DETECTOR_KEY = "axengine"
|
||||||
|
|
||||||
|
supported_models = {
|
||||||
|
ModelTypeEnum.yologeneric: "frigate-yolov9-.*$",
|
||||||
|
}
|
||||||
|
|
||||||
|
model_cache_dir = os.path.join(MODEL_CACHE_DIR, "axengine_cache/")
|
||||||
|
|
||||||
|
|
||||||
|
class AxengineDetectorConfig(BaseDetectorConfig):
|
||||||
|
type: Literal[DETECTOR_KEY]
|
||||||
|
|
||||||
|
|
||||||
|
class Axengine(DetectionApi):
|
||||||
|
type_key = DETECTOR_KEY
|
||||||
|
|
||||||
|
def __init__(self, config: AxengineDetectorConfig):
|
||||||
|
logger.info("__init__ axengine")
|
||||||
|
super().__init__(config)
|
||||||
|
self.height = config.model.height
|
||||||
|
self.width = config.model.width
|
||||||
|
model_path = config.model.path or "frigate-yolov9-tiny"
|
||||||
|
model_props = self.parse_model_input(model_path)
|
||||||
|
self.session = axe.InferenceSession(model_props["path"])
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_model_input(self, model_path):
|
||||||
|
model_props = {}
|
||||||
|
model_props["preset"] = True
|
||||||
|
|
||||||
|
model_matched = False
|
||||||
|
|
||||||
|
for model_type, pattern in supported_models.items():
|
||||||
|
if re.match(pattern, model_path):
|
||||||
|
model_matched = True
|
||||||
|
model_props["model_type"] = model_type
|
||||||
|
|
||||||
|
if model_matched:
|
||||||
|
model_props["filename"] = model_path + ".axmodel"
|
||||||
|
model_props["path"] = model_cache_dir + model_props["filename"]
|
||||||
|
|
||||||
|
if not os.path.isfile(model_props["path"]):
|
||||||
|
self.download_model(model_props["filename"])
|
||||||
|
else:
|
||||||
|
supported_models_str = ", ".join(model[1:-1] for model in supported_models)
|
||||||
|
raise Exception(
|
||||||
|
f"Model {model_path} is unsupported. Provide your own model or choose one of the following: {supported_models_str}"
|
||||||
|
)
|
||||||
|
return model_props
|
||||||
|
|
||||||
|
def download_model(self, filename):
|
||||||
|
if not os.path.isdir(model_cache_dir):
|
||||||
|
os.mkdir(model_cache_dir)
|
||||||
|
|
||||||
|
HF_ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co")
|
||||||
|
urllib.request.urlretrieve(
|
||||||
|
f"{HF_ENDPOINT}/AXERA-TECH/frigate-resource/resolve/axmodel/{filename}",
|
||||||
|
model_cache_dir + filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
def detect_raw(self, tensor_input):
|
||||||
|
results = None
|
||||||
|
results = self.session.run(None, {"images": tensor_input})
|
||||||
|
if self.detector_config.model.model_type == ModelTypeEnum.yologeneric:
|
||||||
|
return post_process_yolo(results, self.width, self.height)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f'Model type "{self.detector_config.model.model_type}" is currently not supported.'
|
||||||
|
)
|
||||||
@ -28,6 +28,7 @@ from frigate.types import ModelStatusTypesEnum
|
|||||||
from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize
|
from frigate.util.builtin import EventsPerSecond, InferenceSpeed, serialize
|
||||||
from frigate.util.file import get_event_thumbnail_bytes
|
from frigate.util.file import get_event_thumbnail_bytes
|
||||||
|
|
||||||
|
from .genai_embedding import GenAIEmbedding
|
||||||
from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding
|
from .onnx.jina_v1_embedding import JinaV1ImageEmbedding, JinaV1TextEmbedding
|
||||||
from .onnx.jina_v2_embedding import JinaV2Embedding
|
from .onnx.jina_v2_embedding import JinaV2Embedding
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ class Embeddings:
|
|||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
db: SqliteVecQueueDatabase,
|
db: SqliteVecQueueDatabase,
|
||||||
metrics: DataProcessorMetrics,
|
metrics: DataProcessorMetrics,
|
||||||
|
genai_manager=None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -104,7 +106,27 @@ class Embeddings:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2:
|
model_cfg = self.config.semantic_search.model
|
||||||
|
|
||||||
|
if not isinstance(model_cfg, SemanticSearchModelEnum):
|
||||||
|
# GenAI provider
|
||||||
|
embeddings_client = (
|
||||||
|
genai_manager.embeddings_client if genai_manager else None
|
||||||
|
)
|
||||||
|
if not embeddings_client:
|
||||||
|
raise ValueError(
|
||||||
|
f"semantic_search.model is '{model_cfg}' (GenAI provider) but "
|
||||||
|
"no embeddings client is configured. Ensure the GenAI provider "
|
||||||
|
"has 'embeddings' in its roles."
|
||||||
|
)
|
||||||
|
self.embedding = GenAIEmbedding(embeddings_client)
|
||||||
|
self.text_embedding = lambda input_data: self.embedding(
|
||||||
|
input_data, embedding_type="text"
|
||||||
|
)
|
||||||
|
self.vision_embedding = lambda input_data: self.embedding(
|
||||||
|
input_data, embedding_type="vision"
|
||||||
|
)
|
||||||
|
elif model_cfg == SemanticSearchModelEnum.jinav2:
|
||||||
# Single JinaV2Embedding instance for both text and vision
|
# Single JinaV2Embedding instance for both text and vision
|
||||||
self.embedding = JinaV2Embedding(
|
self.embedding = JinaV2Embedding(
|
||||||
model_size=self.config.semantic_search.model_size,
|
model_size=self.config.semantic_search.model_size,
|
||||||
@ -118,7 +140,8 @@ class Embeddings:
|
|||||||
self.vision_embedding = lambda input_data: self.embedding(
|
self.vision_embedding = lambda input_data: self.embedding(
|
||||||
input_data, embedding_type="vision"
|
input_data, embedding_type="vision"
|
||||||
)
|
)
|
||||||
else: # Default to jinav1
|
else:
|
||||||
|
# Default to jinav1
|
||||||
self.text_embedding = JinaV1TextEmbedding(
|
self.text_embedding = JinaV1TextEmbedding(
|
||||||
model_size=config.semantic_search.model_size,
|
model_size=config.semantic_search.model_size,
|
||||||
requestor=self.requestor,
|
requestor=self.requestor,
|
||||||
@ -136,8 +159,11 @@ class Embeddings:
|
|||||||
self.metrics.text_embeddings_eps.value = self.text_eps.eps()
|
self.metrics.text_embeddings_eps.value = self.text_eps.eps()
|
||||||
|
|
||||||
def get_model_definitions(self):
|
def get_model_definitions(self):
|
||||||
# Version-specific models
|
model_cfg = self.config.semantic_search.model
|
||||||
if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2:
|
if not isinstance(model_cfg, SemanticSearchModelEnum):
|
||||||
|
# GenAI provider: no ONNX models to download
|
||||||
|
models = []
|
||||||
|
elif model_cfg == SemanticSearchModelEnum.jinav2:
|
||||||
models = [
|
models = [
|
||||||
"jinaai/jina-clip-v2-tokenizer",
|
"jinaai/jina-clip-v2-tokenizer",
|
||||||
"jinaai/jina-clip-v2-model_fp16.onnx"
|
"jinaai/jina-clip-v2-model_fp16.onnx"
|
||||||
@ -312,11 +338,12 @@ class Embeddings:
|
|||||||
# Get total count of events to process
|
# Get total count of events to process
|
||||||
total_events = Event.select().count()
|
total_events = Event.select().count()
|
||||||
|
|
||||||
batch_size = (
|
if not isinstance(self.config.semantic_search.model, SemanticSearchModelEnum):
|
||||||
4
|
batch_size = 1
|
||||||
if self.config.semantic_search.model == SemanticSearchModelEnum.jinav2
|
elif self.config.semantic_search.model == SemanticSearchModelEnum.jinav2:
|
||||||
else 32
|
batch_size = 4
|
||||||
)
|
else:
|
||||||
|
batch_size = 32
|
||||||
current_page = 1
|
current_page = 1
|
||||||
|
|
||||||
totals = {
|
totals = {
|
||||||
|
|||||||
89
frigate/embeddings/genai_embedding.py
Normal file
89
frigate/embeddings/genai_embedding.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""GenAI-backed embeddings for semantic search."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frigate.genai import GenAIClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EMBEDDING_DIM = 768
|
||||||
|
|
||||||
|
|
||||||
|
class GenAIEmbedding:
|
||||||
|
"""Embedding adapter that delegates to a GenAI provider's embed API.
|
||||||
|
|
||||||
|
Provides the same interface as JinaV2Embedding for semantic search:
|
||||||
|
__call__(inputs, embedding_type) -> list[np.ndarray]. Output embeddings are
|
||||||
|
normalized to 768 dimensions for Frigate's sqlite-vec schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, client: "GenAIClient") -> None:
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
inputs: list[str] | list[bytes] | list[Image.Image],
|
||||||
|
embedding_type: str = "text",
|
||||||
|
) -> list[np.ndarray]:
|
||||||
|
"""Generate embeddings for text or images.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inputs: List of strings (text) or bytes/PIL images (vision).
|
||||||
|
embedding_type: "text" or "vision".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of 768-dim numpy float32 arrays.
|
||||||
|
"""
|
||||||
|
if not inputs:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if embedding_type == "text":
|
||||||
|
texts = [str(x) for x in inputs]
|
||||||
|
embeddings = self.client.embed(texts=texts)
|
||||||
|
elif embedding_type == "vision":
|
||||||
|
images: list[bytes] = []
|
||||||
|
for inp in inputs:
|
||||||
|
if isinstance(inp, bytes):
|
||||||
|
images.append(inp)
|
||||||
|
elif isinstance(inp, Image.Image):
|
||||||
|
buf = io.BytesIO()
|
||||||
|
inp.convert("RGB").save(buf, format="JPEG")
|
||||||
|
images.append(buf.getvalue())
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"GenAIEmbedding: skipping unsupported vision input type %s",
|
||||||
|
type(inp).__name__,
|
||||||
|
)
|
||||||
|
if not images:
|
||||||
|
return []
|
||||||
|
embeddings = self.client.embed(images=images)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid embedding_type '{embedding_type}'. Must be 'text' or 'vision'."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for emb in embeddings:
|
||||||
|
arr = np.asarray(emb, dtype=np.float32)
|
||||||
|
if arr.ndim > 1:
|
||||||
|
# Some providers return token-level embeddings; pool to one vector.
|
||||||
|
arr = arr.mean(axis=0)
|
||||||
|
arr = arr.flatten()
|
||||||
|
if arr.size != EMBEDDING_DIM:
|
||||||
|
if arr.size > EMBEDDING_DIM:
|
||||||
|
arr = arr[:EMBEDDING_DIM]
|
||||||
|
else:
|
||||||
|
arr = np.pad(
|
||||||
|
arr,
|
||||||
|
(0, EMBEDDING_DIM - arr.size),
|
||||||
|
mode="constant",
|
||||||
|
constant_values=0,
|
||||||
|
)
|
||||||
|
result.append(arr)
|
||||||
|
return result
|
||||||
@ -96,9 +96,7 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
CameraConfigUpdateEnum.semantic_search,
|
CameraConfigUpdateEnum.semantic_search,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.classification_config_subscriber = ConfigSubscriber(
|
self.enrichment_config_subscriber = ConfigSubscriber("config/")
|
||||||
"config/classification/custom/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure Frigate DB
|
# Configure Frigate DB
|
||||||
db = SqliteVecQueueDatabase(
|
db = SqliteVecQueueDatabase(
|
||||||
@ -116,8 +114,10 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
models = [Event, Recordings, ReviewSegment, Trigger]
|
models = [Event, Recordings, ReviewSegment, Trigger]
|
||||||
db.bind(models)
|
db.bind(models)
|
||||||
|
|
||||||
|
self.genai_manager = GenAIClientManager(config)
|
||||||
|
|
||||||
if config.semantic_search.enabled:
|
if config.semantic_search.enabled:
|
||||||
self.embeddings = Embeddings(config, db, metrics)
|
self.embeddings = Embeddings(config, db, metrics, self.genai_manager)
|
||||||
|
|
||||||
# Check if we need to re-index events
|
# Check if we need to re-index events
|
||||||
if config.semantic_search.reindex:
|
if config.semantic_search.reindex:
|
||||||
@ -144,7 +144,6 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
self.frame_manager = SharedMemoryFrameManager()
|
self.frame_manager = SharedMemoryFrameManager()
|
||||||
|
|
||||||
self.detected_license_plates: dict[str, dict[str, Any]] = {}
|
self.detected_license_plates: dict[str, dict[str, Any]] = {}
|
||||||
self.genai_manager = GenAIClientManager(config)
|
|
||||||
|
|
||||||
# model runners to share between realtime and post processors
|
# model runners to share between realtime and post processors
|
||||||
if self.config.lpr.enabled:
|
if self.config.lpr.enabled:
|
||||||
@ -272,7 +271,7 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
"""Maintain a SQLite-vec database for semantic search."""
|
"""Maintain a SQLite-vec database for semantic search."""
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
self.config_updater.check_for_updates()
|
self.config_updater.check_for_updates()
|
||||||
self._check_classification_config_updates()
|
self._check_enrichment_config_updates()
|
||||||
self._process_requests()
|
self._process_requests()
|
||||||
self._process_updates()
|
self._process_updates()
|
||||||
self._process_recordings_updates()
|
self._process_recordings_updates()
|
||||||
@ -283,7 +282,7 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
self._process_event_metadata()
|
self._process_event_metadata()
|
||||||
|
|
||||||
self.config_updater.stop()
|
self.config_updater.stop()
|
||||||
self.classification_config_subscriber.stop()
|
self.enrichment_config_subscriber.stop()
|
||||||
self.event_subscriber.stop()
|
self.event_subscriber.stop()
|
||||||
self.event_end_subscriber.stop()
|
self.event_end_subscriber.stop()
|
||||||
self.recordings_subscriber.stop()
|
self.recordings_subscriber.stop()
|
||||||
@ -294,67 +293,86 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
self.requestor.stop()
|
self.requestor.stop()
|
||||||
logger.info("Exiting embeddings maintenance...")
|
logger.info("Exiting embeddings maintenance...")
|
||||||
|
|
||||||
def _check_classification_config_updates(self) -> None:
|
def _check_enrichment_config_updates(self) -> None:
|
||||||
"""Check for classification config updates and add/remove processors."""
|
"""Check for enrichment config updates and delegate to processors."""
|
||||||
topic, model_config = self.classification_config_subscriber.check_for_update()
|
topic, payload = self.enrichment_config_subscriber.check_for_update()
|
||||||
|
|
||||||
if topic:
|
if topic is None:
|
||||||
model_name = topic.split("/")[-1]
|
return
|
||||||
|
|
||||||
if model_config is None:
|
# Custom classification add/remove requires managing the processor list
|
||||||
self.realtime_processors = [
|
if topic.startswith("config/classification/custom/"):
|
||||||
processor
|
self._handle_custom_classification_update(topic, payload)
|
||||||
for processor in self.realtime_processors
|
return
|
||||||
if not (
|
|
||||||
isinstance(
|
|
||||||
processor,
|
|
||||||
(
|
|
||||||
CustomStateClassificationProcessor,
|
|
||||||
CustomObjectClassificationProcessor,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
and processor.model_config.name == model_name
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.info(
|
# Broadcast to all processors — each decides if the topic is relevant
|
||||||
f"Successfully removed classification processor for model: {model_name}"
|
for processor in self.realtime_processors:
|
||||||
)
|
processor.update_config(topic, payload)
|
||||||
else:
|
|
||||||
self.config.classification.custom[model_name] = model_config
|
|
||||||
|
|
||||||
# Check if processor already exists
|
for processor in self.post_processors:
|
||||||
for processor in self.realtime_processors:
|
processor.update_config(topic, payload)
|
||||||
if isinstance(
|
|
||||||
|
def _handle_custom_classification_update(
|
||||||
|
self, topic: str, model_config: Any
|
||||||
|
) -> None:
|
||||||
|
"""Handle add/remove of custom classification processors."""
|
||||||
|
model_name = topic.split("/")[-1]
|
||||||
|
|
||||||
|
if model_config is None:
|
||||||
|
self.realtime_processors = [
|
||||||
|
processor
|
||||||
|
for processor in self.realtime_processors
|
||||||
|
if not (
|
||||||
|
isinstance(
|
||||||
processor,
|
processor,
|
||||||
(
|
(
|
||||||
CustomStateClassificationProcessor,
|
CustomStateClassificationProcessor,
|
||||||
CustomObjectClassificationProcessor,
|
CustomObjectClassificationProcessor,
|
||||||
),
|
),
|
||||||
):
|
|
||||||
if processor.model_config.name == model_name:
|
|
||||||
logger.debug(
|
|
||||||
f"Classification processor for model {model_name} already exists, skipping"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if model_config.state_config is not None:
|
|
||||||
processor = CustomStateClassificationProcessor(
|
|
||||||
self.config, model_config, self.requestor, self.metrics
|
|
||||||
)
|
)
|
||||||
else:
|
and processor.model_config.name == model_name
|
||||||
processor = CustomObjectClassificationProcessor(
|
|
||||||
self.config,
|
|
||||||
model_config,
|
|
||||||
self.event_metadata_publisher,
|
|
||||||
self.requestor,
|
|
||||||
self.metrics,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.realtime_processors.append(processor)
|
|
||||||
logger.info(
|
|
||||||
f"Added classification processor for model: {model_name} (type: {type(processor).__name__})"
|
|
||||||
)
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Successfully removed classification processor for model: {model_name}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.config.classification.custom[model_name] = model_config
|
||||||
|
|
||||||
|
# Check if processor already exists
|
||||||
|
for processor in self.realtime_processors:
|
||||||
|
if isinstance(
|
||||||
|
processor,
|
||||||
|
(
|
||||||
|
CustomStateClassificationProcessor,
|
||||||
|
CustomObjectClassificationProcessor,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
if processor.model_config.name == model_name:
|
||||||
|
logger.debug(
|
||||||
|
f"Classification processor for model {model_name} already exists, skipping"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if model_config.state_config is not None:
|
||||||
|
processor = CustomStateClassificationProcessor(
|
||||||
|
self.config, model_config, self.requestor, self.metrics
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
processor = CustomObjectClassificationProcessor(
|
||||||
|
self.config,
|
||||||
|
model_config,
|
||||||
|
self.event_metadata_publisher,
|
||||||
|
self.requestor,
|
||||||
|
self.metrics,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.realtime_processors.append(processor)
|
||||||
|
logger.info(
|
||||||
|
f"Added classification processor for model: {model_name} (type: {type(processor).__name__})"
|
||||||
|
)
|
||||||
|
|
||||||
def _process_requests(self) -> None:
|
def _process_requests(self) -> None:
|
||||||
"""Process embeddings requests"""
|
"""Process embeddings requests"""
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
|
||||||
from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum
|
from frigate.config import CameraConfig, GenAIConfig, GenAIProviderEnum
|
||||||
@ -88,12 +89,7 @@ Your task is to analyze a sequence of images taken in chronological order from a
|
|||||||
|
|
||||||
## Task Instructions
|
## Task Instructions
|
||||||
|
|
||||||
Your task is to provide a clear, accurate description of the scene that:
|
Describe the scene based on observable actions and movements, evaluate the activity against the Activity Indicators above, and assign a potential_threat_level (0, 1, or 2) by applying the threat level indicators consistently.
|
||||||
1. States exactly what is happening based on observable actions and movements.
|
|
||||||
2. Evaluates the activity against the Normal and Suspicious Activity Indicators above.
|
|
||||||
3. Assigns a potential_threat_level (0, 1, or 2) based on the threat level indicators defined above, applying them consistently.
|
|
||||||
|
|
||||||
**Use the activity patterns above as guidance to calibrate your assessment. Match the activity against both normal and suspicious indicators, then use your judgment based on the complete context.**
|
|
||||||
|
|
||||||
## Analysis Guidelines
|
## Analysis Guidelines
|
||||||
|
|
||||||
@ -107,14 +103,12 @@ When forming your description:
|
|||||||
- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
|
- **Consider duration as a primary factor**: Apply the duration thresholds defined in the activity patterns above. Brief sequences during normal hours with apparent purpose typically indicate normal activity unless explicit suspicious actions are visible.
|
||||||
- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
|
- **Weigh all evidence holistically**: Match the activity against the normal and suspicious patterns defined above, then evaluate based on the complete context (zone, objects, time, actions, duration). Apply the threat level indicators consistently. Use your judgment for edge cases.
|
||||||
|
|
||||||
## Response Format
|
## Response Field Guidelines
|
||||||
|
|
||||||
Your response MUST be a flat JSON object with:
|
Respond with a JSON object matching the provided schema. Field-specific guidance:
|
||||||
- `scene` (string): A narrative description of what happens across the sequence from start to finish, in chronological order. Start by describing how the sequence begins, then describe the progression of events. **Describe all significant movements and actions in the order they occur.** For example, if a vehicle arrives and then a person exits, describe both actions sequentially. **Only describe actions you can actually observe happening in the frames provided.** Do not infer or assume actions that aren't visible (e.g., if you see someone walking but never see them sit, don't say they sat down). Include setting, detected objects, and their observable actions. Avoid speculation or filling in assumed behaviors. Your description should align with and support the threat level you assign.
|
- `scene`: Describe how the sequence begins, then the progression of events — all significant movements and actions in order. For example, if a vehicle arrives and then a person exits, describe both sequentially. Your description should align with and support the threat level you assign.
|
||||||
- `title` (string): A concise, grammatically complete title in the format "[Subject] [action verb] [context]" that matches your scene description. Use names from "Objects in Scene" when you visually observe them.
|
- `title`: Characterize **what took place and where** — interpret the overall purpose or outcome, do not simply compress the scene description into fewer words. Include the relevant location (zone, area, or entry point). Always include subject names from "Objects in Scene" — do not replace named subjects with generic terms. No editorial qualifiers like "routine" or "suspicious."
|
||||||
- `shortSummary` (string): A brief 2-sentence summary of the scene, suitable for notifications. Should capture the key activity and context without full detail. This should be a condensed version of the scene description above.
|
- `potential_threat_level`: Must be consistent with your scene description and the activity patterns above.
|
||||||
- `confidence` (float): 0-1 confidence in your analysis. Higher confidence when objects/actions are clearly visible and context is unambiguous. Lower confidence when the sequence is unclear, objects are partially obscured, or context is ambiguous.
|
|
||||||
- `potential_threat_level` (integer): 0, 1, or 2 as defined in "Normal Activity Patterns for This Property" above. Your threat level must be consistent with your scene description and the guidance above.
|
|
||||||
{get_concern_prompt()}
|
{get_concern_prompt()}
|
||||||
|
|
||||||
## Sequence Details
|
## Sequence Details
|
||||||
@ -133,10 +127,6 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
|||||||
**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.**
|
**Note: Unidentified objects (without names) are NOT indicators of suspicious activity—they simply mean the system hasn't identified that object.**
|
||||||
{get_objects_list()}
|
{get_objects_list()}
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
- Values must be plain strings, floats, or integers — no nested objects, no extra commentary.
|
|
||||||
- Only describe objects from the "Objects in Scene" list above. Do not hallucinate additional objects.
|
|
||||||
- When describing people or vehicles, use the exact names provided.
|
|
||||||
{get_language_prompt()}
|
{get_language_prompt()}
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -152,7 +142,27 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
|||||||
) as f:
|
) as f:
|
||||||
f.write(context_prompt)
|
f.write(context_prompt)
|
||||||
|
|
||||||
response = self._send(context_prompt, thumbnails)
|
# Build JSON schema for structured output from ReviewMetadata model
|
||||||
|
schema = ReviewMetadata.model_json_schema()
|
||||||
|
schema.get("properties", {}).pop("time", None)
|
||||||
|
|
||||||
|
if "time" in schema.get("required", []):
|
||||||
|
schema["required"].remove("time")
|
||||||
|
if not concerns:
|
||||||
|
schema.get("properties", {}).pop("other_concerns", None)
|
||||||
|
if "other_concerns" in schema.get("required", []):
|
||||||
|
schema["required"].remove("other_concerns")
|
||||||
|
|
||||||
|
response_format = {
|
||||||
|
"type": "json_schema",
|
||||||
|
"json_schema": {
|
||||||
|
"name": "review_metadata",
|
||||||
|
"strict": True,
|
||||||
|
"schema": schema,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self._send(context_prompt, thumbnails, response_format)
|
||||||
|
|
||||||
if debug_save and response:
|
if debug_save and response:
|
||||||
with open(
|
with open(
|
||||||
@ -171,6 +181,10 @@ Each line represents a detection state, not necessarily unique individuals. Pare
|
|||||||
try:
|
try:
|
||||||
metadata = ReviewMetadata.model_validate_json(clean_json)
|
metadata = ReviewMetadata.model_validate_json(clean_json)
|
||||||
|
|
||||||
|
# Normalize confidence if model returned a percentage (e.g. 85 instead of 0.85)
|
||||||
|
if metadata.confidence > 1.0:
|
||||||
|
metadata.confidence = min(metadata.confidence / 100.0, 1.0)
|
||||||
|
|
||||||
# If any verified objects (contain parentheses with name), set to 0
|
# If any verified objects (contain parentheses with name), set to 0
|
||||||
if any("(" in obj for obj in review_data["unified_objects"]):
|
if any("(" in obj for obj in review_data["unified_objects"]):
|
||||||
metadata.potential_threat_level = 0
|
metadata.potential_threat_level = 0
|
||||||
@ -296,7 +310,12 @@ Guidelines:
|
|||||||
"""Initialize the client."""
|
"""Initialize the client."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to the provider."""
|
"""Submit a request to the provider."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -304,6 +323,25 @@ Guidelines:
|
|||||||
"""Get the context window size for this provider in tokens."""
|
"""Get the context window size for this provider in tokens."""
|
||||||
return 4096
|
return 4096
|
||||||
|
|
||||||
|
def embed(
|
||||||
|
self,
|
||||||
|
texts: list[str] | None = None,
|
||||||
|
images: list[bytes] | None = None,
|
||||||
|
) -> list[np.ndarray]:
|
||||||
|
"""Generate embeddings for text and/or images.
|
||||||
|
|
||||||
|
Returns list of numpy arrays (one per input). Expected dimension is 768
|
||||||
|
for Frigate semantic search compatibility.
|
||||||
|
|
||||||
|
Providers that support embeddings should override this method.
|
||||||
|
"""
|
||||||
|
logger.warning(
|
||||||
|
"%s does not support embeddings. "
|
||||||
|
"This method should be overridden by the provider implementation.",
|
||||||
|
self.__class__.__name__,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
def chat_with_tools(
|
def chat_with_tools(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
|
|||||||
@ -42,13 +42,18 @@ class OpenAIClient(GenAIClient):
|
|||||||
azure_endpoint=azure_endpoint,
|
azure_endpoint=azure_endpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to Azure OpenAI."""
|
"""Submit a request to Azure OpenAI."""
|
||||||
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
|
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
|
||||||
try:
|
try:
|
||||||
result = self.provider.chat.completions.create(
|
request_params = {
|
||||||
model=self.genai_config.model,
|
"model": self.genai_config.model,
|
||||||
messages=[
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": [{"type": "text", "text": prompt}]
|
"content": [{"type": "text", "text": prompt}]
|
||||||
@ -64,9 +69,12 @@ class OpenAIClient(GenAIClient):
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timeout=self.timeout,
|
"timeout": self.timeout,
|
||||||
**self.genai_config.runtime_options,
|
**self.genai_config.runtime_options,
|
||||||
)
|
}
|
||||||
|
if response_format:
|
||||||
|
request_params["response_format"] = response_format
|
||||||
|
result = self.provider.chat.completions.create(**request_params)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Azure OpenAI returned an error: %s", str(e))
|
logger.warning("Azure OpenAI returned an error: %s", str(e))
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -42,7 +42,12 @@ class GeminiClient(GenAIClient):
|
|||||||
http_options=types.HttpOptions(**http_options_dict),
|
http_options=types.HttpOptions(**http_options_dict),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to Gemini."""
|
"""Submit a request to Gemini."""
|
||||||
contents = [
|
contents = [
|
||||||
types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images
|
types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images
|
||||||
@ -52,6 +57,12 @@ class GeminiClient(GenAIClient):
|
|||||||
generation_config_dict = {"candidate_count": 1}
|
generation_config_dict = {"candidate_count": 1}
|
||||||
generation_config_dict.update(self.genai_config.runtime_options)
|
generation_config_dict.update(self.genai_config.runtime_options)
|
||||||
|
|
||||||
|
if response_format and response_format.get("type") == "json_schema":
|
||||||
|
generation_config_dict["response_mime_type"] = "application/json"
|
||||||
|
schema = response_format.get("json_schema", {}).get("schema")
|
||||||
|
if schema:
|
||||||
|
generation_config_dict["response_schema"] = schema
|
||||||
|
|
||||||
response = self.provider.models.generate_content(
|
response = self.provider.models.generate_content(
|
||||||
model=self.genai_config.model,
|
model=self.genai_config.model,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
"""llama.cpp Provider for Frigate AI."""
|
"""llama.cpp Provider for Frigate AI."""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from frigate.config import GenAIProviderEnum
|
from frigate.config import GenAIProviderEnum
|
||||||
from frigate.genai import GenAIClient, register_genai_provider
|
from frigate.genai import GenAIClient, register_genai_provider
|
||||||
@ -15,6 +18,20 @@ from frigate.genai.utils import parse_tool_calls_from_message
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _to_jpeg(img_bytes: bytes) -> bytes | None:
|
||||||
|
"""Convert image bytes to JPEG. llama.cpp/STB does not support WebP."""
|
||||||
|
try:
|
||||||
|
img = Image.open(io.BytesIO(img_bytes))
|
||||||
|
if img.mode != "RGB":
|
||||||
|
img = img.convert("RGB")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format="JPEG", quality=85)
|
||||||
|
return buf.getvalue()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to convert image to JPEG: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register_genai_provider(GenAIProviderEnum.llamacpp)
|
@register_genai_provider(GenAIProviderEnum.llamacpp)
|
||||||
class LlamaCppClient(GenAIClient):
|
class LlamaCppClient(GenAIClient):
|
||||||
"""Generative AI client for Frigate using llama.cpp server."""
|
"""Generative AI client for Frigate using llama.cpp server."""
|
||||||
@ -40,7 +57,12 @@ class LlamaCppClient(GenAIClient):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to llama.cpp server."""
|
"""Submit a request to llama.cpp server."""
|
||||||
if self.provider is None:
|
if self.provider is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -79,6 +101,9 @@ class LlamaCppClient(GenAIClient):
|
|||||||
**self.provider_options,
|
**self.provider_options,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if response_format:
|
||||||
|
payload["response_format"] = response_format
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.provider}/v1/chat/completions",
|
f"{self.provider}/v1/chat/completions",
|
||||||
json=payload,
|
json=payload,
|
||||||
@ -176,6 +201,110 @@ class LlamaCppClient(GenAIClient):
|
|||||||
)
|
)
|
||||||
return result if result else None
|
return result if result else None
|
||||||
|
|
||||||
|
def embed(
|
||||||
|
self,
|
||||||
|
texts: list[str] | None = None,
|
||||||
|
images: list[bytes] | None = None,
|
||||||
|
) -> list[np.ndarray]:
|
||||||
|
"""Generate embeddings via llama.cpp /embeddings endpoint.
|
||||||
|
|
||||||
|
Supports batch requests. Uses content format with prompt_string and
|
||||||
|
multimodal_data for images (PR #15108). Server must be started with
|
||||||
|
--embeddings and --mmproj for multimodal support.
|
||||||
|
"""
|
||||||
|
if self.provider is None:
|
||||||
|
logger.warning(
|
||||||
|
"llama.cpp provider has not been initialized. Check your llama.cpp configuration."
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
texts = texts or []
|
||||||
|
images = images or []
|
||||||
|
if not texts and not images:
|
||||||
|
return []
|
||||||
|
|
||||||
|
EMBEDDING_DIM = 768
|
||||||
|
|
||||||
|
content = []
|
||||||
|
for text in texts:
|
||||||
|
content.append({"prompt_string": text})
|
||||||
|
for img in images:
|
||||||
|
# llama.cpp uses STB which does not support WebP; convert to JPEG
|
||||||
|
jpeg_bytes = _to_jpeg(img)
|
||||||
|
to_encode = jpeg_bytes if jpeg_bytes is not None else img
|
||||||
|
encoded = base64.b64encode(to_encode).decode("utf-8")
|
||||||
|
# prompt_string must contain <__media__> placeholder for image tokenization
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"prompt_string": "<__media__>\n",
|
||||||
|
"multimodal_data": [encoded],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.provider}/embeddings",
|
||||||
|
json={"model": self.genai_config.model, "content": content},
|
||||||
|
timeout=self.timeout,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
items = result.get("data", result) if isinstance(result, dict) else result
|
||||||
|
if not isinstance(items, list):
|
||||||
|
logger.warning("llama.cpp embeddings returned unexpected format")
|
||||||
|
return []
|
||||||
|
|
||||||
|
embeddings = []
|
||||||
|
for item in items:
|
||||||
|
emb = item.get("embedding") if isinstance(item, dict) else None
|
||||||
|
if emb is None:
|
||||||
|
logger.warning("llama.cpp embeddings item missing embedding field")
|
||||||
|
continue
|
||||||
|
arr = np.array(emb, dtype=np.float32)
|
||||||
|
if arr.ndim > 1:
|
||||||
|
# llama.cpp can return token-level embeddings; pool per item
|
||||||
|
arr = arr.mean(axis=0)
|
||||||
|
arr = arr.flatten()
|
||||||
|
orig_dim = arr.size
|
||||||
|
if orig_dim != EMBEDDING_DIM:
|
||||||
|
if orig_dim > EMBEDDING_DIM:
|
||||||
|
arr = arr[:EMBEDDING_DIM]
|
||||||
|
logger.debug(
|
||||||
|
"Truncated llama.cpp embedding from %d to %d dimensions",
|
||||||
|
orig_dim,
|
||||||
|
EMBEDDING_DIM,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
arr = np.pad(
|
||||||
|
arr,
|
||||||
|
(0, EMBEDDING_DIM - orig_dim),
|
||||||
|
mode="constant",
|
||||||
|
constant_values=0,
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
"Padded llama.cpp embedding from %d to %d dimensions",
|
||||||
|
orig_dim,
|
||||||
|
EMBEDDING_DIM,
|
||||||
|
)
|
||||||
|
embeddings.append(arr)
|
||||||
|
return embeddings
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("llama.cpp embeddings request timed out")
|
||||||
|
return []
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_detail = str(e)
|
||||||
|
if hasattr(e, "response") and e.response is not None:
|
||||||
|
try:
|
||||||
|
error_detail = f"{str(e)} - Response: {e.response.text[:500]}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.warning("llama.cpp embeddings error: %s", error_detail)
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Unexpected error in llama.cpp embeddings: %s", str(e))
|
||||||
|
return []
|
||||||
|
|
||||||
def chat_with_tools(
|
def chat_with_tools(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
|
|||||||
@ -53,7 +53,12 @@ class OllamaClient(GenAIClient):
|
|||||||
logger.warning("Error initializing Ollama: %s", str(e))
|
logger.warning("Error initializing Ollama: %s", str(e))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to Ollama"""
|
"""Submit a request to Ollama"""
|
||||||
if self.provider is None:
|
if self.provider is None:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -65,6 +70,10 @@ class OllamaClient(GenAIClient):
|
|||||||
**self.provider_options,
|
**self.provider_options,
|
||||||
**self.genai_config.runtime_options,
|
**self.genai_config.runtime_options,
|
||||||
}
|
}
|
||||||
|
if response_format and response_format.get("type") == "json_schema":
|
||||||
|
schema = response_format.get("json_schema", {}).get("schema")
|
||||||
|
if schema:
|
||||||
|
ollama_options["format"] = schema
|
||||||
result = self.provider.generate(
|
result = self.provider.generate(
|
||||||
self.genai_config.model,
|
self.genai_config.model,
|
||||||
prompt,
|
prompt,
|
||||||
|
|||||||
@ -36,7 +36,12 @@ class OpenAIClient(GenAIClient):
|
|||||||
|
|
||||||
return OpenAI(api_key=self.genai_config.api_key, **provider_opts)
|
return OpenAI(api_key=self.genai_config.api_key, **provider_opts)
|
||||||
|
|
||||||
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
|
def _send(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
images: list[bytes],
|
||||||
|
response_format: Optional[dict] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
"""Submit a request to OpenAI."""
|
"""Submit a request to OpenAI."""
|
||||||
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
|
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
|
||||||
messages_content = []
|
messages_content = []
|
||||||
@ -57,17 +62,20 @@ class OpenAIClient(GenAIClient):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = self.provider.chat.completions.create(
|
request_params = {
|
||||||
model=self.genai_config.model,
|
"model": self.genai_config.model,
|
||||||
messages=[
|
"messages": [
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": messages_content,
|
"content": messages_content,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timeout=self.timeout,
|
"timeout": self.timeout,
|
||||||
**self.genai_config.runtime_options,
|
**self.genai_config.runtime_options,
|
||||||
)
|
}
|
||||||
|
if response_format:
|
||||||
|
request_params["response_format"] = response_format
|
||||||
|
result = self.provider.chat.completions.create(**request_params)
|
||||||
if (
|
if (
|
||||||
result is not None
|
result is not None
|
||||||
and hasattr(result, "choices")
|
and hasattr(result, "choices")
|
||||||
|
|||||||
@ -273,17 +273,13 @@ class BirdsEyeFrameManager:
|
|||||||
stop_event: mp.Event,
|
stop_event: mp.Event,
|
||||||
):
|
):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.mode = config.birdseye.mode
|
|
||||||
width, height = get_canvas_shape(config.birdseye.width, config.birdseye.height)
|
width, height = get_canvas_shape(config.birdseye.width, config.birdseye.height)
|
||||||
self.frame_shape = (height, width)
|
self.frame_shape = (height, width)
|
||||||
self.yuv_shape = (height * 3 // 2, width)
|
self.yuv_shape = (height * 3 // 2, width)
|
||||||
self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
|
self.frame = np.ndarray(self.yuv_shape, dtype=np.uint8)
|
||||||
self.canvas = Canvas(width, height, config.birdseye.layout.scaling_factor)
|
self.canvas = Canvas(width, height, config.birdseye.layout.scaling_factor)
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.inactivity_threshold = config.birdseye.inactivity_threshold
|
self.last_refresh_time = 0
|
||||||
|
|
||||||
if config.birdseye.layout.max_cameras:
|
|
||||||
self.last_refresh_time = 0
|
|
||||||
|
|
||||||
# initialize the frame as black and with the Frigate logo
|
# initialize the frame as black and with the Frigate logo
|
||||||
self.blank_frame = np.zeros(self.yuv_shape, np.uint8)
|
self.blank_frame = np.zeros(self.yuv_shape, np.uint8)
|
||||||
@ -426,7 +422,7 @@ class BirdsEyeFrameManager:
|
|||||||
and self.config.cameras[cam].enabled
|
and self.config.cameras[cam].enabled
|
||||||
and cam_data["last_active_frame"] > 0
|
and cam_data["last_active_frame"] > 0
|
||||||
and cam_data["current_frame_time"] - cam_data["last_active_frame"]
|
and cam_data["current_frame_time"] - cam_data["last_active_frame"]
|
||||||
< self.inactivity_threshold
|
< self.config.birdseye.inactivity_threshold
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
logger.debug(f"Active cameras: {active_cameras}")
|
logger.debug(f"Active cameras: {active_cameras}")
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from ws4py.server.wsgirefserver import (
|
|||||||
)
|
)
|
||||||
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
||||||
|
|
||||||
|
from frigate.comms.config_updater import ConfigSubscriber
|
||||||
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
|
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
|
||||||
from frigate.comms.ws import WebSocket
|
from frigate.comms.ws import WebSocket
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
@ -140,6 +141,7 @@ class OutputProcess(FrigateProcess):
|
|||||||
CameraConfigUpdateEnum.record,
|
CameraConfigUpdateEnum.record,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
birdseye_config_subscriber = ConfigSubscriber("config/birdseye", exact=True)
|
||||||
|
|
||||||
jsmpeg_cameras: dict[str, JsmpegCamera] = {}
|
jsmpeg_cameras: dict[str, JsmpegCamera] = {}
|
||||||
birdseye: Birdseye | None = None
|
birdseye: Birdseye | None = None
|
||||||
@ -169,6 +171,20 @@ class OutputProcess(FrigateProcess):
|
|||||||
websocket_thread.start()
|
websocket_thread.start()
|
||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
|
update_topic, birdseye_config = (
|
||||||
|
birdseye_config_subscriber.check_for_update()
|
||||||
|
)
|
||||||
|
|
||||||
|
if update_topic is not None:
|
||||||
|
previous_global_mode = self.config.birdseye.mode
|
||||||
|
self.config.birdseye = birdseye_config
|
||||||
|
|
||||||
|
for camera_config in self.config.cameras.values():
|
||||||
|
if camera_config.birdseye.mode == previous_global_mode:
|
||||||
|
camera_config.birdseye.mode = birdseye_config.mode
|
||||||
|
|
||||||
|
logger.debug("Applied dynamic birdseye config update")
|
||||||
|
|
||||||
# check if there is an updated config
|
# check if there is an updated config
|
||||||
updates = config_subscriber.check_for_updates()
|
updates = config_subscriber.check_for_updates()
|
||||||
|
|
||||||
@ -299,6 +315,7 @@ class OutputProcess(FrigateProcess):
|
|||||||
birdseye.stop()
|
birdseye.stop()
|
||||||
|
|
||||||
config_subscriber.stop()
|
config_subscriber.stop()
|
||||||
|
birdseye_config_subscriber.stop()
|
||||||
websocket_server.manager.close_all()
|
websocket_server.manager.close_all()
|
||||||
websocket_server.manager.stop()
|
websocket_server.manager.stop()
|
||||||
websocket_server.manager.join()
|
websocket_server.manager.join()
|
||||||
|
|||||||
261
frigate/test/http_api/test_http_config_set.py
Normal file
261
frigate/test/http_api/test_http_config_set.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
"""Tests for the config_set endpoint's wildcard camera propagation."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
|
import ruamel.yaml
|
||||||
|
|
||||||
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.config.camera.updater import (
|
||||||
|
CameraConfigUpdateEnum,
|
||||||
|
CameraConfigUpdatePublisher,
|
||||||
|
CameraConfigUpdateTopic,
|
||||||
|
)
|
||||||
|
from frigate.models import Event, Recordings, ReviewSegment
|
||||||
|
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfigSetWildcardPropagation(BaseTestHttp):
|
||||||
|
"""Test that wildcard camera updates fan out to all cameras."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp(models=[Event, Recordings, ReviewSegment])
|
||||||
|
self.minimal_config = {
|
||||||
|
"mqtt": {"host": "mqtt"},
|
||||||
|
"cameras": {
|
||||||
|
"front_door": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"height": 1080,
|
||||||
|
"width": 1920,
|
||||||
|
"fps": 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"back_yard": {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"height": 720,
|
||||||
|
"width": 1280,
|
||||||
|
"fps": 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_app_with_publisher(self):
|
||||||
|
"""Create app with a mocked config publisher."""
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||||
|
from frigate.api.fastapi_app import create_fastapi_app
|
||||||
|
|
||||||
|
mock_publisher = Mock(spec=CameraConfigUpdatePublisher)
|
||||||
|
mock_publisher.publisher = MagicMock()
|
||||||
|
|
||||||
|
app = create_fastapi_app(
|
||||||
|
FrigateConfig(**self.minimal_config),
|
||||||
|
self.db,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
mock_publisher,
|
||||||
|
None,
|
||||||
|
enforce_default_admin=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def mock_get_current_user(request: Request):
|
||||||
|
username = request.headers.get("remote-user")
|
||||||
|
role = request.headers.get("remote-role")
|
||||||
|
return {"username": username, "role": role}
|
||||||
|
|
||||||
|
async def mock_get_allowed_cameras_for_filter(request: Request):
|
||||||
|
return list(self.minimal_config.get("cameras", {}).keys())
|
||||||
|
|
||||||
|
app.dependency_overrides[get_current_user] = mock_get_current_user
|
||||||
|
app.dependency_overrides[get_allowed_cameras_for_filter] = (
|
||||||
|
mock_get_allowed_cameras_for_filter
|
||||||
|
)
|
||||||
|
|
||||||
|
return app, mock_publisher
|
||||||
|
|
||||||
|
def _write_config_file(self):
|
||||||
|
"""Write the minimal config to a temp YAML file and return the path."""
|
||||||
|
yaml = ruamel.yaml.YAML()
|
||||||
|
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False)
|
||||||
|
yaml.dump(self.minimal_config, f)
|
||||||
|
f.close()
|
||||||
|
return f.name
|
||||||
|
|
||||||
|
@patch("frigate.api.app.find_config_file")
|
||||||
|
def test_wildcard_detect_update_fans_out_to_all_cameras(self, mock_find_config):
|
||||||
|
"""config/cameras/*/detect fans out to all cameras."""
|
||||||
|
config_path = self._write_config_file()
|
||||||
|
mock_find_config.return_value = config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, mock_publisher = self._create_app_with_publisher()
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
resp = client.put(
|
||||||
|
"/config/set",
|
||||||
|
json={
|
||||||
|
"config_data": {"detect": {"fps": 15}},
|
||||||
|
"update_topic": "config/cameras/*/detect",
|
||||||
|
"requires_restart": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertTrue(data["success"])
|
||||||
|
|
||||||
|
# Verify publish_update called for each camera
|
||||||
|
self.assertEqual(mock_publisher.publish_update.call_count, 2)
|
||||||
|
|
||||||
|
published_cameras = set()
|
||||||
|
for c in mock_publisher.publish_update.call_args_list:
|
||||||
|
topic = c[0][0]
|
||||||
|
self.assertIsInstance(topic, CameraConfigUpdateTopic)
|
||||||
|
self.assertEqual(topic.update_type, CameraConfigUpdateEnum.detect)
|
||||||
|
published_cameras.add(topic.camera)
|
||||||
|
|
||||||
|
self.assertEqual(published_cameras, {"front_door", "back_yard"})
|
||||||
|
|
||||||
|
# Global publisher should NOT be called for wildcard
|
||||||
|
mock_publisher.publisher.publish.assert_not_called()
|
||||||
|
finally:
|
||||||
|
os.unlink(config_path)
|
||||||
|
|
||||||
|
@patch("frigate.api.app.find_config_file")
|
||||||
|
def test_wildcard_motion_update_fans_out(self, mock_find_config):
|
||||||
|
"""config/cameras/*/motion fans out to all cameras."""
|
||||||
|
config_path = self._write_config_file()
|
||||||
|
mock_find_config.return_value = config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, mock_publisher = self._create_app_with_publisher()
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
resp = client.put(
|
||||||
|
"/config/set",
|
||||||
|
json={
|
||||||
|
"config_data": {"motion": {"threshold": 30}},
|
||||||
|
"update_topic": "config/cameras/*/motion",
|
||||||
|
"requires_restart": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
published_cameras = set()
|
||||||
|
for c in mock_publisher.publish_update.call_args_list:
|
||||||
|
topic = c[0][0]
|
||||||
|
self.assertEqual(topic.update_type, CameraConfigUpdateEnum.motion)
|
||||||
|
published_cameras.add(topic.camera)
|
||||||
|
|
||||||
|
self.assertEqual(published_cameras, {"front_door", "back_yard"})
|
||||||
|
finally:
|
||||||
|
os.unlink(config_path)
|
||||||
|
|
||||||
|
@patch("frigate.api.app.find_config_file")
|
||||||
|
def test_camera_specific_topic_only_updates_one_camera(self, mock_find_config):
|
||||||
|
"""config/cameras/front_door/detect only updates front_door."""
|
||||||
|
config_path = self._write_config_file()
|
||||||
|
mock_find_config.return_value = config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, mock_publisher = self._create_app_with_publisher()
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
resp = client.put(
|
||||||
|
"/config/set",
|
||||||
|
json={
|
||||||
|
"config_data": {
|
||||||
|
"cameras": {"front_door": {"detect": {"fps": 20}}}
|
||||||
|
},
|
||||||
|
"update_topic": "config/cameras/front_door/detect",
|
||||||
|
"requires_restart": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
# Only one camera updated
|
||||||
|
self.assertEqual(mock_publisher.publish_update.call_count, 1)
|
||||||
|
topic = mock_publisher.publish_update.call_args[0][0]
|
||||||
|
self.assertEqual(topic.camera, "front_door")
|
||||||
|
self.assertEqual(topic.update_type, CameraConfigUpdateEnum.detect)
|
||||||
|
|
||||||
|
# Global publisher should NOT be called
|
||||||
|
mock_publisher.publisher.publish.assert_not_called()
|
||||||
|
finally:
|
||||||
|
os.unlink(config_path)
|
||||||
|
|
||||||
|
@patch("frigate.api.app.find_config_file")
|
||||||
|
def test_wildcard_sends_merged_per_camera_config(self, mock_find_config):
|
||||||
|
"""Wildcard fan-out sends each camera's own merged config."""
|
||||||
|
config_path = self._write_config_file()
|
||||||
|
mock_find_config.return_value = config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, mock_publisher = self._create_app_with_publisher()
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
resp = client.put(
|
||||||
|
"/config/set",
|
||||||
|
json={
|
||||||
|
"config_data": {"detect": {"fps": 15}},
|
||||||
|
"update_topic": "config/cameras/*/detect",
|
||||||
|
"requires_restart": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
for c in mock_publisher.publish_update.call_args_list:
|
||||||
|
camera_detect_config = c[0][1]
|
||||||
|
self.assertIsNotNone(camera_detect_config)
|
||||||
|
self.assertTrue(hasattr(camera_detect_config, "fps"))
|
||||||
|
finally:
|
||||||
|
os.unlink(config_path)
|
||||||
|
|
||||||
|
@patch("frigate.api.app.find_config_file")
|
||||||
|
def test_non_camera_global_topic_uses_generic_publish(self, mock_find_config):
|
||||||
|
"""Non-camera topics (e.g. config/live) use the generic publisher."""
|
||||||
|
config_path = self._write_config_file()
|
||||||
|
mock_find_config.return_value = config_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
app, mock_publisher = self._create_app_with_publisher()
|
||||||
|
with AuthTestClient(app) as client:
|
||||||
|
resp = client.put(
|
||||||
|
"/config/set",
|
||||||
|
json={
|
||||||
|
"config_data": {"live": {"height": 720}},
|
||||||
|
"update_topic": "config/live",
|
||||||
|
"requires_restart": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
# Global topic publisher called
|
||||||
|
mock_publisher.publisher.publish.assert_called_once()
|
||||||
|
|
||||||
|
# Camera-level publish_update NOT called
|
||||||
|
mock_publisher.publish_update.assert_not_called()
|
||||||
|
finally:
|
||||||
|
os.unlink(config_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
153
frigate/util/camera_cleanup.py
Normal file
153
frigate/util/camera_cleanup.py
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"""Utilities for cleaning up camera data from database and filesystem."""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from frigate.const import CLIPS_DIR, RECORD_DIR, THUMB_DIR
|
||||||
|
from frigate.models import (
|
||||||
|
Event,
|
||||||
|
Export,
|
||||||
|
Previews,
|
||||||
|
Recordings,
|
||||||
|
Regions,
|
||||||
|
ReviewSegment,
|
||||||
|
Timeline,
|
||||||
|
Trigger,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_camera_db(
|
||||||
|
camera_name: str, delete_exports: bool = False
|
||||||
|
) -> tuple[dict[str, int], list[str]]:
|
||||||
|
"""Remove all database rows for a camera.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_name: The camera name to clean up
|
||||||
|
delete_exports: Whether to also delete export records
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (deletion counts dict, list of export file paths to remove)
|
||||||
|
"""
|
||||||
|
counts: dict[str, int] = {}
|
||||||
|
export_paths: list[str] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["events"] = Event.delete().where(Event.camera == camera_name).execute()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete events for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["timeline"] = (
|
||||||
|
Timeline.delete().where(Timeline.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete timeline for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["recordings"] = (
|
||||||
|
Recordings.delete().where(Recordings.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete recordings for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["review_segments"] = (
|
||||||
|
ReviewSegment.delete().where(ReviewSegment.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Failed to delete review segments for camera %s: %s", camera_name, e
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["previews"] = (
|
||||||
|
Previews.delete().where(Previews.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete previews for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["regions"] = (
|
||||||
|
Regions.delete().where(Regions.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete regions for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
counts["triggers"] = (
|
||||||
|
Trigger.delete().where(Trigger.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete triggers for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
if delete_exports:
|
||||||
|
try:
|
||||||
|
exports = Export.select(Export.video_path, Export.thumb_path).where(
|
||||||
|
Export.camera == camera_name
|
||||||
|
)
|
||||||
|
for export in exports:
|
||||||
|
export_paths.append(export.video_path)
|
||||||
|
export_paths.append(export.thumb_path)
|
||||||
|
|
||||||
|
counts["exports"] = (
|
||||||
|
Export.delete().where(Export.camera == camera_name).execute()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to delete exports for camera %s: %s", camera_name, e)
|
||||||
|
|
||||||
|
return counts, export_paths
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_camera_files(
|
||||||
|
camera_name: str, export_paths: list[str] | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Remove filesystem artifacts for a camera.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_name: The camera name to clean up
|
||||||
|
export_paths: Optional list of export file paths to remove
|
||||||
|
"""
|
||||||
|
dirs_to_clean = [
|
||||||
|
os.path.join(RECORD_DIR, camera_name),
|
||||||
|
os.path.join(CLIPS_DIR, camera_name),
|
||||||
|
os.path.join(THUMB_DIR, camera_name),
|
||||||
|
os.path.join(CLIPS_DIR, "previews", camera_name),
|
||||||
|
]
|
||||||
|
|
||||||
|
for dir_path in dirs_to_clean:
|
||||||
|
if os.path.exists(dir_path):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(dir_path)
|
||||||
|
logger.debug("Removed directory: %s", dir_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to remove %s: %s", dir_path, e)
|
||||||
|
|
||||||
|
# Remove event snapshot files
|
||||||
|
for snapshot in glob.glob(os.path.join(CLIPS_DIR, f"{camera_name}-*.jpg")):
|
||||||
|
try:
|
||||||
|
os.remove(snapshot)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to remove snapshot %s: %s", snapshot, e)
|
||||||
|
|
||||||
|
# Remove review thumbnail files
|
||||||
|
for thumb in glob.glob(
|
||||||
|
os.path.join(CLIPS_DIR, "review", f"thumb-{camera_name}-*.webp")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
os.remove(thumb)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to remove review thumbnail %s: %s", thumb, e)
|
||||||
|
|
||||||
|
# Remove export files if requested
|
||||||
|
if export_paths:
|
||||||
|
for path in export_paths:
|
||||||
|
if path and os.path.exists(path):
|
||||||
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
logger.debug("Removed export file: %s", path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to remove export file %s: %s", path, e)
|
||||||
@ -155,7 +155,9 @@ def sync_recordings(
|
|||||||
|
|
||||||
max_inserts = 1000
|
max_inserts = 1000
|
||||||
for batch in chunked(recordings_to_delete, max_inserts):
|
for batch in chunked(recordings_to_delete, max_inserts):
|
||||||
RecordingsToDelete.insert_many(batch).execute()
|
RecordingsToDelete.insert_many(
|
||||||
|
[{"id": r["id"]} for r in batch]
|
||||||
|
).execute()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
deleted = (
|
deleted = (
|
||||||
|
|||||||
@ -779,7 +779,7 @@ async def get_video_properties(
|
|||||||
duration = float(duration_str) if duration_str else -1.0
|
duration = float(duration_str) if duration_str else -1.0
|
||||||
|
|
||||||
return True, width, height, codec, duration
|
return True, width, height, codec, duration
|
||||||
except (json.JSONDecodeError, ValueError, KeyError, asyncio.SubprocessError):
|
except (json.JSONDecodeError, ValueError, KeyError, sp.SubprocessError):
|
||||||
return False, 0, 0, None, -1
|
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, Optional[str], float]:
|
||||||
|
|||||||
@ -214,7 +214,11 @@ class CameraWatchdog(threading.Thread):
|
|||||||
self.config_subscriber = CameraConfigUpdateSubscriber(
|
self.config_subscriber = CameraConfigUpdateSubscriber(
|
||||||
None,
|
None,
|
||||||
{config.name: config},
|
{config.name: config},
|
||||||
[CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record],
|
[
|
||||||
|
CameraConfigUpdateEnum.enabled,
|
||||||
|
CameraConfigUpdateEnum.ffmpeg,
|
||||||
|
CameraConfigUpdateEnum.record,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
self.requestor = InterProcessRequestor()
|
self.requestor = InterProcessRequestor()
|
||||||
self.was_enabled = self.config.enabled
|
self.was_enabled = self.config.enabled
|
||||||
@ -254,9 +258,13 @@ class CameraWatchdog(threading.Thread):
|
|||||||
self._last_record_status = status
|
self._last_record_status = status
|
||||||
self._last_status_update_time = now
|
self._last_status_update_time = now
|
||||||
|
|
||||||
|
def _check_config_updates(self) -> dict[str, list[str]]:
|
||||||
|
"""Check for config updates and return the update dict."""
|
||||||
|
return self.config_subscriber.check_for_updates()
|
||||||
|
|
||||||
def _update_enabled_state(self) -> bool:
|
def _update_enabled_state(self) -> bool:
|
||||||
"""Fetch the latest config and update enabled state."""
|
"""Fetch the latest config and update enabled state."""
|
||||||
self.config_subscriber.check_for_updates()
|
self._check_config_updates()
|
||||||
return self.config.enabled
|
return self.config.enabled
|
||||||
|
|
||||||
def reset_capture_thread(
|
def reset_capture_thread(
|
||||||
@ -317,7 +325,24 @@ class CameraWatchdog(threading.Thread):
|
|||||||
|
|
||||||
# 1 second watchdog loop
|
# 1 second watchdog loop
|
||||||
while not self.stop_event.wait(1):
|
while not self.stop_event.wait(1):
|
||||||
enabled = self._update_enabled_state()
|
updates = self._check_config_updates()
|
||||||
|
|
||||||
|
# Handle ffmpeg config changes by restarting all ffmpeg processes
|
||||||
|
if "ffmpeg" in updates and self.config.enabled:
|
||||||
|
self.logger.debug(
|
||||||
|
"FFmpeg config updated for %s, restarting ffmpeg processes",
|
||||||
|
self.config.name,
|
||||||
|
)
|
||||||
|
self.stop_all_ffmpeg()
|
||||||
|
self.start_all_ffmpeg()
|
||||||
|
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)
|
||||||
|
last_restart_time = datetime.now().timestamp()
|
||||||
|
continue
|
||||||
|
|
||||||
|
enabled = self.config.enabled
|
||||||
if enabled != self.was_enabled:
|
if enabled != self.was_enabled:
|
||||||
if enabled:
|
if enabled:
|
||||||
self.logger.debug(f"Enabling camera {self.config.name}")
|
self.logger.debug(f"Enabling camera {self.config.name}")
|
||||||
|
|||||||
72
web/package-lock.json
generated
72
web/package-lock.json
generated
@ -72,8 +72,6 @@
|
|||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.30.3",
|
"react-router-dom": "^6.30.3",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
"react-tracked": "^2.0.1",
|
|
||||||
"react-use-websocket": "^4.8.1",
|
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
@ -4400,7 +4398,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.3.1.tgz",
|
||||||
"integrity": "sha512-LTjFz5Fk3FlbgFPJ+OJi1JdWJyiap9dSpx8W6u7JHNB7K5VbwzJe8gIU45XWLHzWFGDHKPm89VrUzjOs07TPtg==",
|
"integrity": "sha512-LTjFz5Fk3FlbgFPJ+OJi1JdWJyiap9dSpx8W6u7JHNB7K5VbwzJe8gIU45XWLHzWFGDHKPm89VrUzjOs07TPtg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
@ -4475,7 +4472,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.3.1.tgz",
|
||||||
"integrity": "sha512-ve2KHl1ITYG8QIonnuK83/T1k/5NuxP4D1egVqP9Hz2ub28kgl0rNMwmRSxXs3WIbCcMW9g3ox+daVrbSNc4Mw==",
|
"integrity": "sha512-ve2KHl1ITYG8QIonnuK83/T1k/5NuxP4D1egVqP9Hz2ub28kgl0rNMwmRSxXs3WIbCcMW9g3ox+daVrbSNc4Mw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@x0k/json-schema-merge": "^1.0.2",
|
"@x0k/json-schema-merge": "^1.0.2",
|
||||||
"fast-uri": "^3.1.0",
|
"fast-uri": "^3.1.0",
|
||||||
@ -5149,7 +5145,6 @@
|
|||||||
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
@ -5159,7 +5154,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@ -5170,7 +5164,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@ -5300,7 +5293,6 @@
|
|||||||
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
|
"integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "7.12.0",
|
"@typescript-eslint/scope-manager": "7.12.0",
|
||||||
"@typescript-eslint/types": "7.12.0",
|
"@typescript-eslint/types": "7.12.0",
|
||||||
@ -5593,7 +5585,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -5755,7 +5746,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
|
||||||
"integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
|
"integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yr/monotone-cubic-spline": "^1.0.3",
|
"@yr/monotone-cubic-spline": "^1.0.3",
|
||||||
"svg.draggable.js": "^2.2.2",
|
"svg.draggable.js": "^2.2.2",
|
||||||
@ -5966,7 +5956,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001646",
|
"caniuse-lite": "^1.0.30001646",
|
||||||
"electron-to-chromium": "^1.5.4",
|
"electron-to-chromium": "^1.5.4",
|
||||||
@ -6467,7 +6456,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@ -6907,7 +6895,6 @@
|
|||||||
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@ -6963,7 +6950,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
@ -7887,7 +7873,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
},
|
},
|
||||||
@ -8533,8 +8518,7 @@
|
|||||||
"url": "https://github.com/sponsors/lavrton"
|
"url": "https://github.com/sponsors/lavrton"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
@ -9683,8 +9667,7 @@
|
|||||||
"version": "0.52.2",
|
"version": "0.52.2",
|
||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/monaco-languageserver-types": {
|
"node_modules/monaco-languageserver-types": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
@ -10392,7 +10375,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.8",
|
"nanoid": "^3.3.8",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@ -10527,7 +10509,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@ -10676,11 +10657,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-compare": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-y44MCkgtZUCT9tZGuE278fB7PWVf7fRYy0vbRXAts2o5F0EfC4fIQrvQQGBJo1WJbFcVLXzApOscyJuZqHQc1w=="
|
|
||||||
},
|
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@ -10733,7 +10709,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -10798,7 +10773,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@ -10860,7 +10834,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.1.tgz",
|
||||||
"integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==",
|
"integrity": "sha512-uNKIhaoICJ5KQALYZ4TOaOLElyM+xipord+Ha3crEFhTntdLvWZqVY49Wqd/0GiVCA/f9NjemLeiNPjG7Hpurg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.22.0"
|
"node": ">=12.22.0"
|
||||||
},
|
},
|
||||||
@ -11115,29 +11088,6 @@
|
|||||||
"react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc"
|
"react": "^16.8.3 || ^17 || ^18 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-tracked": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-tracked/-/react-tracked-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-qjbmtkO2IcW+rB2cFskRWDTjKs/w9poxvNnduacjQA04LWxOoLy9J8WfIEq1ahifQ/tVJQECrQPBm+UEzKRDtg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"proxy-compare": "^3.0.0",
|
|
||||||
"use-context-selector": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"scheduler": ">=0.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-use-websocket": {
|
|
||||||
"version": "4.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz",
|
|
||||||
"integrity": "sha512-FTXuG5O+LFozmu1BRfrzl7UIQngECvGJmL7BHsK4TYXuVt+mCizVA8lT0hGSIF0Z0TedF7bOo1nRzOUdginhDw==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">= 18.0.0",
|
|
||||||
"react-dom": ">= 18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-zoom-pan-pinch": {
|
"node_modules/react-zoom-pan-pinch": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz",
|
||||||
@ -11549,8 +11499,7 @@
|
|||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/scroll-into-view-if-needed": {
|
"node_modules/scroll-into-view-if-needed": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@ -12049,7 +11998,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
|
||||||
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
|
"integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@ -12232,7 +12180,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -12411,7 +12358,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -12627,15 +12573,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-context-selector": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/use-context-selector/-/use-context-selector-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"scheduler": ">=0.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/use-long-press": {
|
"node_modules/use-long-press": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-long-press/-/use-long-press-3.2.0.tgz",
|
||||||
@ -12771,7 +12708,6 @@
|
|||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
@ -12896,7 +12832,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@ -12910,7 +12845,6 @@
|
|||||||
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "3.0.7",
|
"@vitest/expect": "3.0.7",
|
||||||
"@vitest/mocker": "3.0.7",
|
"@vitest/mocker": "3.0.7",
|
||||||
|
|||||||
@ -78,8 +78,6 @@
|
|||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^6.30.3",
|
"react-router-dom": "^6.30.3",
|
||||||
"react-swipeable": "^7.0.2",
|
"react-swipeable": "^7.0.2",
|
||||||
"react-tracked": "^2.0.1",
|
|
||||||
"react-use-websocket": "^4.8.1",
|
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
diff --git a/node_modules/react-use-websocket/dist/lib/use-websocket.js b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
|
||||||
index f01db48..b30aff2 100644
|
|
||||||
--- a/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
|
||||||
+++ b/node_modules/react-use-websocket/dist/lib/use-websocket.js
|
|
||||||
@@ -139,15 +139,15 @@ var useWebSocket = function (url, options, connect) {
|
|
||||||
}
|
|
||||||
protectedSetLastMessage = function (message) {
|
|
||||||
if (!expectClose_1) {
|
|
||||||
- (0, react_dom_1.flushSync)(function () { return setLastMessage(message); });
|
|
||||||
+ setLastMessage(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
protectedSetReadyState = function (state) {
|
|
||||||
if (!expectClose_1) {
|
|
||||||
- (0, react_dom_1.flushSync)(function () { return setReadyState(function (prev) {
|
|
||||||
+ setReadyState(function (prev) {
|
|
||||||
var _a;
|
|
||||||
return (__assign(__assign({}, prev), (convertedUrl.current && (_a = {}, _a[convertedUrl.current] = state, _a))));
|
|
||||||
- }); });
|
|
||||||
+ });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (createOrJoin_1) {
|
|
||||||
@ -106,7 +106,9 @@
|
|||||||
"logout": "Tanca la sessió",
|
"logout": "Tanca la sessió",
|
||||||
"current": "Usuari actual: {{user}}"
|
"current": "Usuari actual: {{user}}"
|
||||||
},
|
},
|
||||||
"classification": "Classificació"
|
"classification": "Classificació",
|
||||||
|
"chat": "Xat",
|
||||||
|
"actions": "Accions"
|
||||||
},
|
},
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"previous": {
|
"previous": {
|
||||||
@ -268,7 +270,18 @@
|
|||||||
"unselect": "Desseleccionar",
|
"unselect": "Desseleccionar",
|
||||||
"enable": "Habilitar",
|
"enable": "Habilitar",
|
||||||
"enabled": "Habilitat",
|
"enabled": "Habilitat",
|
||||||
"continue": "Continua"
|
"continue": "Continua",
|
||||||
|
"add": "Afegeix",
|
||||||
|
"undo": "Desfés",
|
||||||
|
"copiedToClipboard": "S'ha copiat al porta-retalls",
|
||||||
|
"modified": "Modificat",
|
||||||
|
"overridden": "Sobreescrit",
|
||||||
|
"resetToGlobal": "Restableix a global",
|
||||||
|
"resetToDefault": "Restableix al valor predeterminat",
|
||||||
|
"saveAll": "Desa-ho tot",
|
||||||
|
"savingAll": "S'està desant tot…",
|
||||||
|
"undoAll": "Desfés-ho tot",
|
||||||
|
"applying": "S'està aplicant…"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"copyUrlToClipboard": "URL copiada al porta-retalls.",
|
"copyUrlToClipboard": "URL copiada al porta-retalls.",
|
||||||
|
|||||||
@ -65,6 +65,10 @@
|
|||||||
"fromTimeline": {
|
"fromTimeline": {
|
||||||
"saveExport": "Guardar exportació",
|
"saveExport": "Guardar exportació",
|
||||||
"previewExport": "Previsualitzar exportació"
|
"previewExport": "Previsualitzar exportació"
|
||||||
|
},
|
||||||
|
"case": {
|
||||||
|
"label": "Cas",
|
||||||
|
"placeholder": "Selecciona un cas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"streaming": {
|
"streaming": {
|
||||||
|
|||||||
941
web/public/locales/ca/config/cameras.json
Normal file
941
web/public/locales/ca/config/cameras.json
Normal file
@ -0,0 +1,941 @@
|
|||||||
|
{
|
||||||
|
"label": "ConfiguracióDeLaCcàmera",
|
||||||
|
"name": {
|
||||||
|
"label": "Nom de la càmera",
|
||||||
|
"description": "Es requereix el nom de la càmera"
|
||||||
|
},
|
||||||
|
"friendly_name": {
|
||||||
|
"label": "Nom amistós",
|
||||||
|
"description": "Nom amigable de la càmera utilitzat a la interfície d'usuari de la Frigate"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilitat",
|
||||||
|
"description": "Habilitat"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"label": "Esdeveniments d'àudio",
|
||||||
|
"description": "Configuració per a la detecció d'esdeveniments basats en àudio per a aquesta càmera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita la detecció d'àudio",
|
||||||
|
"description": "Activa o desactiva la detecció d'esdeveniments d'àudio per a aquesta càmera."
|
||||||
|
},
|
||||||
|
"max_not_heard": {
|
||||||
|
"label": "Temps d'espera final",
|
||||||
|
"description": "Quantitat de segons sense el tipus d'àudio configurat abans que acabi l'esdeveniment d'àudio."
|
||||||
|
},
|
||||||
|
"min_volume": {
|
||||||
|
"label": "Volum mínim",
|
||||||
|
"description": "Llindar mínim de volum RMS necessari per executar la detecció d'àudio; els valors més baixos augmenten la sensibilitat (p. ex., 200 alta, 500 mitjana, 1000 baixa)."
|
||||||
|
},
|
||||||
|
"listen": {
|
||||||
|
"label": "Tipus d'escoltes",
|
||||||
|
"description": "Llista de tipus d'esdeveniment d'àudio a detectar (per exemple: escorça, focarmalarma, crit, parla, crida)."
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filtres d'àudio",
|
||||||
|
"description": "Paràmetres de filtre per-àudio-tipus, com ara llindars de confiança utilitzats per reduir falsos positius."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat d'àudio original",
|
||||||
|
"description": "Indica si la detecció d'àudio s'ha activat originalment al fitxer de configuració estàtic."
|
||||||
|
},
|
||||||
|
"num_threads": {
|
||||||
|
"label": "Fils de detecció",
|
||||||
|
"description": "Nombre de fils a utilitzar per al processament de detecció d'àudio."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio_transcription": {
|
||||||
|
"label": "Transcripció d'àudio",
|
||||||
|
"description": "Configuració per a la transcripció d'àudio en viu i de veu utilitzada per a esdeveniments i llegendes en directe.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita la transcripció",
|
||||||
|
"description": "Activa o desactiva la transcripció d'esdeveniments d'àudio activada manualment."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de transcripció original"
|
||||||
|
},
|
||||||
|
"live_enabled": {
|
||||||
|
"label": "Transcripció en viu",
|
||||||
|
"description": "Habilita la transcripció en directe per a l'àudio a mesura que es rep."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birdseye": {
|
||||||
|
"label": "Birdseye",
|
||||||
|
"description": "Arranjament per a la vista composta Birdseye que compon múltiples canals de càmera en una única disposició.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita Birdseye",
|
||||||
|
"description": "Activa o desactiva la funció de vista Birdseye."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Mode de seguiment",
|
||||||
|
"description": "Mode per a incloure càmeres en Birdseye: 'objectes', 'motion' o 'continuous'."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"label": "Posició",
|
||||||
|
"description": "Posició numèrica que controla l'ordenació de la càmera en la disposició Birdseye."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"label": "Detecció d'objectes",
|
||||||
|
"description": "Configuració del rol de detecció utilitzat per executar la detecció d'objectes i inicialitzar els rastrejadors.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Detecció activada",
|
||||||
|
"description": "Activa o desactiva la detecció d'objectes per a aquesta càmera. La detecció s'ha d'activar perquè s'executi el seguiment d'objectes."
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"label": "Detecta l'alçada",
|
||||||
|
"description": "Alçada (píxels) dels fotogrames utilitzats per al flux de detecció; deixeu-ho buit per a utilitzar la resolució nativa del flux."
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"label": "Detecta l'amplada",
|
||||||
|
"description": "Amplada (píxels) dels fotogrames utilitzats per al flux de detecció; deixeu-ho buit per a utilitzar la resolució nativa del flux."
|
||||||
|
},
|
||||||
|
"fps": {
|
||||||
|
"label": "Detecta FPS",
|
||||||
|
"description": "Fotogrames desitjats per segon per executar la detecció; els valors més baixos redueixen l'ús de la CPU (el valor recomanat és 5, només estableix més alt - com a màxim 10 - si el seguiment d'objectes en moviment extremadament ràpid)."
|
||||||
|
},
|
||||||
|
"min_initialized": {
|
||||||
|
"label": "Fotogrames d'inicialització mínims",
|
||||||
|
"description": "Nombre d'incidències de detecció consecutives necessàries abans de crear un objecte rastrejat. Incrementa per a reduir les falses inicialitzacions. El valor per defecte és fps dividit per 2."
|
||||||
|
},
|
||||||
|
"max_disappeared": {
|
||||||
|
"label": "Màxim de fotogrames desapareguts",
|
||||||
|
"description": "Nombre de fotogrames sense detecció abans que es consideri que un objecte rastrejat ha desaparegut."
|
||||||
|
},
|
||||||
|
"stationary": {
|
||||||
|
"label": "Configuració d'objectes estacionaris",
|
||||||
|
"description": "Configuració per detectar i gestionar objectes que romanen estacionaris durant un període de temps.",
|
||||||
|
"interval": {
|
||||||
|
"label": "Interval estacionari",
|
||||||
|
"description": "Amb quina freqüència (en fotogrames) s'executa una comprovació de detecció per confirmar un objecte estacionari."
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"label": "Llindar estacionari",
|
||||||
|
"description": "Nombre de fotogrames sense cap canvi de posició necessari per a marcar un objecte com a estacionari."
|
||||||
|
},
|
||||||
|
"max_frames": {
|
||||||
|
"label": "Fotogrames màxims",
|
||||||
|
"description": "Limita quant de temps es segueixen els objectes estacionaris abans de descartar-los.",
|
||||||
|
"default": {
|
||||||
|
"label": "Fotogrames màxims predeterminats",
|
||||||
|
"description": "Fotogrames màxims predeterminats per a fer el seguiment d'un objecte estacionari abans d'aturar-se."
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"label": "Fotogrames màxims de l'objecte",
|
||||||
|
"description": "Sobreescriu l'objecte per als fotogrames màxims per fer un seguiment dels objectes estacionaris."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"classifier": {
|
||||||
|
"label": "Habilita el classificador visual",
|
||||||
|
"description": "Utilitzeu un classificador visual per detectar objectes realment estacionaris, fins i tot quan les caixes contenidores tremolen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"annotation_offset": {
|
||||||
|
"label": "Desplaçament de l'anotació",
|
||||||
|
"description": "Mil·lisegons per a desplaçar detecta anotacions per a alinear millor els límits de la línia de temps amb els enregistraments; pot ser positiu o negatiu."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"face_recognition": {
|
||||||
|
"label": "Reconeixement de cares",
|
||||||
|
"description": "Configuració per a la detecció de la cara i el reconeixement d'aquesta càmera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita el reconeixement facial",
|
||||||
|
"description": "Activa o desactiva el reconeixement facial."
|
||||||
|
},
|
||||||
|
"min_area": {
|
||||||
|
"label": "Àrea mínima de la cara",
|
||||||
|
"description": "Àrea mínima (píxels) d'un quadre facial detectat requerit per intentar el reconeixement."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"label": "FFmpeg",
|
||||||
|
"description": "Paràmetres del FFmpeg que inclouen camins 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»)."
|
||||||
|
},
|
||||||
|
"global_args": {
|
||||||
|
"label": "Arguments globals del FFmpeg",
|
||||||
|
"description": "Arguments globals passats als processos FFmpeg."
|
||||||
|
},
|
||||||
|
"hwaccel_args": {
|
||||||
|
"label": "Arguments d'acceleració del maquinari",
|
||||||
|
"description": "Arguments d'acceleració de maquinari per a FFmpeg. Es recomanen predefinits específics del proveïdor."
|
||||||
|
},
|
||||||
|
"input_args": {
|
||||||
|
"label": "Arguments d'entrada",
|
||||||
|
"description": "Arguments d'entrada aplicats als fluxos d'entrada del FFmpeg."
|
||||||
|
},
|
||||||
|
"output_args": {
|
||||||
|
"label": "Arguments de sortida",
|
||||||
|
"description": "Arguments de sortida predeterminats utilitzats per a diferents rols FFmpeg com detecta i registra.",
|
||||||
|
"detect": {
|
||||||
|
"label": "Detecta els arguments de sortida",
|
||||||
|
"description": "Arguments de sortida predeterminats per a detectar fluxos de rol."
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"label": "Registra els arguments de sortida",
|
||||||
|
"description": "Arguments de sortida predeterminats per a enregistrar fluxos de rols."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retry_interval": {
|
||||||
|
"label": "Temps de reintent del FFmpeg",
|
||||||
|
"description": "Segons a esperar abans d'intentar tornar a connectar un flux de càmera després d'un error. Per defecte és 10."
|
||||||
|
},
|
||||||
|
"apple_compatibility": {
|
||||||
|
"label": "Compatibilitat d'Apple",
|
||||||
|
"description": "Activa l'etiquetatge HEVC per a una millor compatibilitat amb el reproductor d'Apple en gravar H.265."
|
||||||
|
},
|
||||||
|
"gpu": {
|
||||||
|
"label": "Índex de GPU",
|
||||||
|
"description": "Índex de GPU predeterminat utilitzat per a l'acceleració de maquinari si està disponible."
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"label": "Entrada de la càmera",
|
||||||
|
"description": "Llista de definicions de flux d'entrada (camins i rols) per a aquesta càmera.",
|
||||||
|
"path": {
|
||||||
|
"label": "Ruta d'entrada",
|
||||||
|
"description": "URL o camí del flux d'entrada de la càmera."
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"label": "Rols d'entrada",
|
||||||
|
"description": "Rols per a aquest flux d'entrada."
|
||||||
|
},
|
||||||
|
"global_args": {
|
||||||
|
"label": "Arguments globals del FFmpeg",
|
||||||
|
"description": "Arguments globals del FFmpeg per a aquest flux d'entrada."
|
||||||
|
},
|
||||||
|
"hwaccel_args": {
|
||||||
|
"label": "Arguments d'acceleració del maquinari",
|
||||||
|
"description": "Arguments d'acceleració del maquinari per a aquest flux d'entrada."
|
||||||
|
},
|
||||||
|
"input_args": {
|
||||||
|
"label": "Arguments d'entrada",
|
||||||
|
"description": "Arguments d'entrada específics d'aquest flux."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"live": {
|
||||||
|
"label": "Reproducció en directe",
|
||||||
|
"description": "Configuració utilitzada per la interfície d'usuari web per controlar la selecció, resolució i qualitat del flux en viu.",
|
||||||
|
"streams": {
|
||||||
|
"label": "Noms de flux en viu",
|
||||||
|
"description": "Assignació de noms de flux configurats per a restream/go2rtc noms utilitzats per a la reproducció en viu."
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"label": "Alçada del directe",
|
||||||
|
"description": "Alçada (píxels) per a renderitzar el flux en viu jsmpeg a la interfície d'usuari web; ha de ser . detecta l'alçada del flux."
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "Qualitat del directe",
|
||||||
|
"description": "Qualitat de codificació per al flux jsmpeg (1 més alt, 31 més baix)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lpr": {
|
||||||
|
"label": "Reconeixement de la placa de llicència",
|
||||||
|
"description": "Paràmetres de reconeixement de la matrícula de la llicència, inclosos els llindars de detecció, el format i les plaques conegudes.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita el LPR",
|
||||||
|
"description": "Activa o desactiva LPR en aquesta càmera."
|
||||||
|
},
|
||||||
|
"expire_time": {
|
||||||
|
"label": "Caduca els segons",
|
||||||
|
"description": "Temps en segons després del qual una placa no vista expira del rastrejador (només per a càmeres LPR dedicades)."
|
||||||
|
},
|
||||||
|
"min_area": {
|
||||||
|
"label": "Àrea mínima de la placa",
|
||||||
|
"description": "Àrea mínima de placa (píxels) necessària per intentar el reconeixement."
|
||||||
|
},
|
||||||
|
"enhancement": {
|
||||||
|
"label": "Nivell de millora",
|
||||||
|
"description": "Nivell de millora (0-10) per aplicar als cultius de plaques abans de l'OCR; els valors més alts no sempre poden millorar els resultats, els nivells superiors a 5 només poden funcionar amb plaques nocturnes i s'han d'utilitzar amb precaució."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"label": "Detecció de moviment",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita la detecció de moviment",
|
||||||
|
"description": "Activa o desactiva la detecció de moviment d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"description": "Configuració predeterminada de detecció de moviment per a aquesta càmera.",
|
||||||
|
"threshold": {
|
||||||
|
"label": "Llindar del moviment",
|
||||||
|
"description": "Llindar de diferència de píxels utilitzat pel detector de moviment; els valors més alts redueixen la sensibilitat (interval 1-255)."
|
||||||
|
},
|
||||||
|
"lightning_threshold": {
|
||||||
|
"label": "Llindar del llamp",
|
||||||
|
"description": "Llindar per detectar i ignorar les puntes d'il·luminació breu (més baixes són més sensibles, valors entre 0,3 i 1,0). Això no impedeix la detecció de moviment per complet; simplement fa que el detector deixi d'analitzar fotogrames addicionals una vegada que el llindar s'excedeix. Els enregistraments basats en moviment encara es creen durant aquests esdeveniments."
|
||||||
|
},
|
||||||
|
"improve_contrast": {
|
||||||
|
"label": "Millora el contrast",
|
||||||
|
"description": "Aplicar la millora del contrast als fotogrames abans de l'anàlisi del moviment per ajudar a la detecció."
|
||||||
|
},
|
||||||
|
"contour_area": {
|
||||||
|
"label": "Àrea de la vora",
|
||||||
|
"description": "Àrea mínima de contorn en píxels necessària per a comptar un contorn de moviment."
|
||||||
|
},
|
||||||
|
"delta_alpha": {
|
||||||
|
"label": "Delta alfa",
|
||||||
|
"description": "Factor de barreja alfa utilitzat en la diferència de fotogrames per al càlcul del moviment."
|
||||||
|
},
|
||||||
|
"frame_alpha": {
|
||||||
|
"label": "Alfa del fotograma",
|
||||||
|
"description": "Valor alfa utilitzat en la barreja de fotogrames per al preprocessament del moviment."
|
||||||
|
},
|
||||||
|
"frame_height": {
|
||||||
|
"label": "Alçada del marc",
|
||||||
|
"description": "Alçada en píxels per a escalar els fotogrames quan es computa el moviment."
|
||||||
|
},
|
||||||
|
"mask": {
|
||||||
|
"label": "Coordenades de la màscara",
|
||||||
|
"description": "Coordenades x,y que defineixen el polígon de màscara de moviment utilitzat per incloure/excloure àrees."
|
||||||
|
},
|
||||||
|
"mqtt_off_delay": {
|
||||||
|
"label": "Retard MQTT desactivat",
|
||||||
|
"description": "Segons a esperar després de l'última moció abans de publicar un estat MQTT 'off'."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat del moviment original",
|
||||||
|
"description": "Indica si la detecció de moviment s'ha activat en la configuració estàtica original."
|
||||||
|
},
|
||||||
|
"raw_mask": {
|
||||||
|
"label": "Màscara en brut"
|
||||||
|
},
|
||||||
|
"skip_motion_threshold": {
|
||||||
|
"label": "Omet el llindar de moviment",
|
||||||
|
"description": "Si més d'aquesta fracció de la imatge canvia en un sol fotograma, el detector no retornarà cap caixa de moviment i recalibrarà immediatament. Això pot estalviar CPU i reduir falsos positius durant el llamp, tempestes, etc., però pot perdre esdeveniments reals com una càmera PTZ que fa un seguiment automàtic d'un objecte. La compensació es troba entre deixar caure uns quants megabytes d'enregistraments versus revisar un parell de clips curts. Interval de 0,0 a 1,0."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"label": "Objectes",
|
||||||
|
"description": "Object tracking defaults incloent quines etiquetes rastrejar i per objecte filtres.",
|
||||||
|
"track": {
|
||||||
|
"label": "Objectes a seguir",
|
||||||
|
"description": "Llista d'etiquetes d'objectes a seguir per a aquesta càmera."
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filtres d'objectes",
|
||||||
|
"description": "Filtres aplicats als objectes detectats per reduir falsos positius (àrea, relació, confiança).",
|
||||||
|
"min_area": {
|
||||||
|
"label": "Àrea mínima de l'objecte",
|
||||||
|
"description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest tipus d'objecte. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
|
||||||
|
},
|
||||||
|
"max_area": {
|
||||||
|
"label": "Àrea màxima de l'objecte",
|
||||||
|
"description": "Es permet l'àrea màxima de la caixa contenidora (píxels o percentatge) per a aquest tipus d'objecte. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
|
||||||
|
},
|
||||||
|
"min_ratio": {
|
||||||
|
"label": "Relació mínima d'aspecte",
|
||||||
|
"description": "Relació mínima d'amplada/alçada requerida per a la casella contenidora a qualificar."
|
||||||
|
},
|
||||||
|
"max_ratio": {
|
||||||
|
"description": "Es permet la relació màxima d'amplada/alçada per a la casella contenidora a qualificar.",
|
||||||
|
"label": "Relació màxima d'aspecte"
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"label": "Llindar de confiança",
|
||||||
|
"description": "Es requereix un llindar de confiança mitjà per a la detecció perquè l'objecte es consideri un veritable positiu."
|
||||||
|
},
|
||||||
|
"min_score": {
|
||||||
|
"label": "Confiança mínima",
|
||||||
|
"description": "Es requereix una confiança mínima de detecció d'un sol fotograma per a comptar l'objecte."
|
||||||
|
},
|
||||||
|
"mask": {
|
||||||
|
"label": "Màscara de filtre",
|
||||||
|
"description": "Coordenades de polígon que defineixen on s'aplica aquest filtre dins del marc."
|
||||||
|
},
|
||||||
|
"raw_mask": {
|
||||||
|
"label": "Màscara en brut"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mask": {
|
||||||
|
"label": "Màscara d'objecte",
|
||||||
|
"description": "Polígon de màscara utilitzat per evitar la detecció d'objectes en àrees especificades."
|
||||||
|
},
|
||||||
|
"raw_mask": {
|
||||||
|
"label": "Màscara en brut"
|
||||||
|
},
|
||||||
|
"genai": {
|
||||||
|
"label": "Configuració de l'objecte GenAI",
|
||||||
|
"description": "Opcions de GenAI per descriure objectes rastrejats i enviar fotogrames per a la generació.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita el GenAI",
|
||||||
|
"description": "Habilita la generació de descripcions de GenAI per als objectes rastrejats de manera predeterminada."
|
||||||
|
},
|
||||||
|
"use_snapshot": {
|
||||||
|
"label": "Utilitza instantànies",
|
||||||
|
"description": "Usa instantànies d'objecte en lloc de miniatures per a la generació de descripcions de GenAI."
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"label": "Indicació de la llegenda",
|
||||||
|
"description": "Plantilla de pregunta predeterminada utilitzada en generar descripcions amb GenAI."
|
||||||
|
},
|
||||||
|
"object_prompts": {
|
||||||
|
"label": "Peticions d'objecte",
|
||||||
|
"description": "Per objecte demana personalitzar les sortides de GenAI per a etiquetes específiques."
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"label": "Objectes GenAI",
|
||||||
|
"description": "Llista d'etiquetes d'objectes a enviar a GenAI per defecte."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Zones que s'han d'introduir perquè els objectes es puguin classificar per a la generació de descripcions de GenAI."
|
||||||
|
},
|
||||||
|
"debug_save_thumbnails": {
|
||||||
|
"label": "Desa les miniatures",
|
||||||
|
"description": "Desa les miniatures enviades a GenAI per a la depuració i la revisió."
|
||||||
|
},
|
||||||
|
"send_triggers": {
|
||||||
|
"label": "Activadors de GenAI",
|
||||||
|
"description": "Defineix quan s'han d'enviar fotogrames a GenAI (al final, després de les actualitzacions, etc.).",
|
||||||
|
"tracked_object_end": {
|
||||||
|
"label": "Envia al final",
|
||||||
|
"description": "Envia una sol·licitud a GenAI quan acabi l'objecte rastrejat."
|
||||||
|
},
|
||||||
|
"after_significant_updates": {
|
||||||
|
"label": "Activador de GenAI primerenc",
|
||||||
|
"description": "Envia una sol·licitud a GenAI després d'un nombre especificat d'actualitzacions significatives per a l'objecte rastrejat."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat original de GenAI",
|
||||||
|
"description": "Indica si el GenAI s'ha activat a la configuració estàtica original."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"label": "Enregistrament",
|
||||||
|
"description": "Configuració d'enregistrament i retenció d'aquesta càmera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita l'enregistrament",
|
||||||
|
"description": "Activa o desactiva l'enregistrament d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"expire_interval": {
|
||||||
|
"label": "Interval de neteja de l'enregistrament",
|
||||||
|
"description": "Minuts entre passades de neteja que eliminen segments d'enregistrament caducats."
|
||||||
|
},
|
||||||
|
"continuous": {
|
||||||
|
"label": "Retenció contínua",
|
||||||
|
"description": "Nombre de dies per a retenir els enregistraments independentment dels objectes rastrejats o del moviment. Establiu-ho a 0 si només voleu retenir enregistraments d'alertes i deteccions.",
|
||||||
|
"days": {
|
||||||
|
"label": "Dies de retenció",
|
||||||
|
"description": "Dies per retenir enregistraments."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"label": "Retenció del moviment",
|
||||||
|
"description": "Nombre de dies per a retenir els enregistraments activats pel moviment independentment dels objectes rastrejats. Establiu-ho a 0 si només voleu retenir enregistraments d'alertes i deteccions.",
|
||||||
|
"days": {
|
||||||
|
"label": "Dies de retenció",
|
||||||
|
"description": "Dies per retenir enregistraments."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detections": {
|
||||||
|
"label": "Retenció de detecció",
|
||||||
|
"description": "Configuració de retenció de l'enregistrament per a esdeveniments de detecció, incloent-hi la durada de la captura anterior a la publicació.",
|
||||||
|
"pre_capture": {
|
||||||
|
"label": "Segons de precaptura",
|
||||||
|
"description": "Nombre de segons abans de l'esdeveniment de detecció a incloure en l'enregistrament."
|
||||||
|
},
|
||||||
|
"post_capture": {
|
||||||
|
"label": "Segons de postcaptura",
|
||||||
|
"description": "Nombre de segons després de l'esdeveniment de detecció que s'inclourà a l'enregistrament."
|
||||||
|
},
|
||||||
|
"retain": {
|
||||||
|
"label": "Retenció d'esdeveniments",
|
||||||
|
"description": "Configuració de retenció per a enregistraments d'esdeveniments de detecció.",
|
||||||
|
"days": {
|
||||||
|
"label": "Dies de retenció",
|
||||||
|
"description": "Nombre de dies per a retenir enregistraments d'esdeveniments de detecció."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Mode de retenció",
|
||||||
|
"description": "Mode de retenció: tot (desa tots els segments), moviment (desa els segments amb moviment), o actiuobobjectes (desa els segments amb objectes actius)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"label": "Retenció d'alerta",
|
||||||
|
"description": "Configuració de retenció de l'enregistrament per a esdeveniments d'alerta, incloses les durades de captura anteriors a la publicació.",
|
||||||
|
"pre_capture": {
|
||||||
|
"label": "Segons de precaptura",
|
||||||
|
"description": "Nombre de segons abans de l'esdeveniment de detecció a incloure en l'enregistrament."
|
||||||
|
},
|
||||||
|
"post_capture": {
|
||||||
|
"label": "Segons de postcaptura",
|
||||||
|
"description": "Nombre de segons després de l'esdeveniment de detecció que s'inclourà a l'enregistrament."
|
||||||
|
},
|
||||||
|
"retain": {
|
||||||
|
"label": "Retenció d'esdeveniments",
|
||||||
|
"description": "Configuració de retenció per a enregistraments d'esdeveniments de detecció.",
|
||||||
|
"days": {
|
||||||
|
"label": "Dies de retenció",
|
||||||
|
"description": "Nombre de dies per a retenir enregistraments d'esdeveniments de detecció."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Mode de retenció",
|
||||||
|
"description": "Mode de retenció: tot (desa tots els segments), moviment (desa els segments amb moviment), o actiuobobjectes (desa els segments amb objectes actius)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"label": "Exporta la configuració",
|
||||||
|
"description": "Paràmetres utilitzats en exportar enregistraments com el timelapse i l'acceleració del maquinari.",
|
||||||
|
"hwaccel_args": {
|
||||||
|
"label": "Exporta els arguments de l'hwaccel",
|
||||||
|
"description": "Args d'acceleració de maquinari a utilitzar per a operacions d'exportació/transcodificació."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"label": "Configuració de la vista prèvia",
|
||||||
|
"description": "Paràmetres que controlen la qualitat de les vistes prèvies de l'enregistrament que es mostren a la interfície d'usuari.",
|
||||||
|
"quality": {
|
||||||
|
"label": "Qualitat de la vista prèvia",
|
||||||
|
"description": "Nivell de qualitat de la vista prèvia (moltlowbaix, baix, mitjà, alt, molt).alt)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de l'enregistrament original",
|
||||||
|
"description": "Indica si l'enregistrament s'ha activat en la configuració estàtica original."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"label": "Revisió",
|
||||||
|
"description": "Configuració que controla les alertes, les deteccions i els resums de revisió de GenAI utilitzats per la interfície d'usuari i l'emmagatzematge d'aquesta càmera.",
|
||||||
|
"alerts": {
|
||||||
|
"label": "Configuració d'alertes",
|
||||||
|
"description": "Paràmetres per als quals els objectes rastrejats generen alertes i com es mantenen les alertes",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita les alertes",
|
||||||
|
"description": "Activa o desactiva la generació d'alertes per a aquesta càmera."
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"label": "Etiquetes d'alerta",
|
||||||
|
"description": "Llista d'etiquetes d'objectes que qualifiquen d'alertes (per exemple: cotxe, persona)."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Zones que un objecte ha d'introduir per a ser considerat una alerta; deixeu-ho buit per a permetre qualsevol zona."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de les alertes originals",
|
||||||
|
"description": "Fa un seguiment de si les alertes es van habilitar originalment a la configuració estàtica."
|
||||||
|
},
|
||||||
|
"cutoff_time": {
|
||||||
|
"label": "Temps de tall d'alertes",
|
||||||
|
"description": "Segons a esperar després de no provocar activitat d'alerta abans de tallar una alerta."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detections": {
|
||||||
|
"label": "Configuració de les deteccions",
|
||||||
|
"description": "Paràmetres per a crear esdeveniments de detecció (no-alerta) i quant de temps conservar-los.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita les deteccions",
|
||||||
|
"description": "Activa o desactiva els esdeveniments de detecció d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"label": "Etiquetes de detecció",
|
||||||
|
"description": "Llista d'etiquetes d'objectes que es qualifiquen com a esdeveniments de detecció."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Zones que un objecte ha d'introduir per a ser considerat una detecció; deixeu-ho buit per a permetre qualsevol zona."
|
||||||
|
},
|
||||||
|
"cutoff_time": {
|
||||||
|
"label": "Temps de tall de detecció",
|
||||||
|
"description": "Segons a esperar després de no haver-hi activitat de detecció abans de tallar una detecció"
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de les deteccions originals",
|
||||||
|
"description": "Fa un seguiment de si les deteccions es van habilitar originalment a la configuració estàtica."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"genai": {
|
||||||
|
"label": "Configuració del GenAI",
|
||||||
|
"description": "Controla l'ús de la IA generativa per a la producció de descripcions i resums d'articles de revisió.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita les descripcions del GenAI",
|
||||||
|
"description": "Activa o desactiva les descripcions i resums generats per GenAI per als elements de revisió."
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"label": "Habilita el GenAI per a alertes",
|
||||||
|
"description": "Utilitzeu GenAI per a generar descripcions per als elements d'alerta."
|
||||||
|
},
|
||||||
|
"detections": {
|
||||||
|
"label": "Habilita el GenAI per a les deteccions",
|
||||||
|
"description": "Utilitzeu GenAI per generar descripcions per als elements de detecció."
|
||||||
|
},
|
||||||
|
"image_source": {
|
||||||
|
"label": "Revisa l'origen de la imatge",
|
||||||
|
"description": "Font d'imatges enviades a GenAI ('previsualització' o 'enregistraments'); 'enregistraments' utilitza Fotogrames de més qualitat però més tokens."
|
||||||
|
},
|
||||||
|
"additional_concerns": {
|
||||||
|
"label": "Altres preocupacions",
|
||||||
|
"description": "Una llista de preocupacions o notes addicionals que el GenAI ha de tenir en compte a l'hora d'avaluar l'activitat en aquesta càmera."
|
||||||
|
},
|
||||||
|
"debug_save_thumbnails": {
|
||||||
|
"label": "Desa les miniatures",
|
||||||
|
"description": "Desa les miniatures que s'envien al proveïdor GenAI per a la depuració i la revisió."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat original de GenAI",
|
||||||
|
"description": "Fa un seguiment de si la revisió de GenAI es va habilitar originalment a la configuració estàtica."
|
||||||
|
},
|
||||||
|
"preferred_language": {
|
||||||
|
"label": "Idioma preferit",
|
||||||
|
"description": "Idioma preferit per sol·licitar al proveïdor GenAI respostes generades."
|
||||||
|
},
|
||||||
|
"activity_context_prompt": {
|
||||||
|
"label": "Indicador de context de l'activitat",
|
||||||
|
"description": "Pregunta personalitzada que descriu el que és i no és una activitat sospitosa per proporcionar context per als resums de GenAI."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"semantic_search": {
|
||||||
|
"label": "Cerca semàntica",
|
||||||
|
"description": "Paràmetres per a la cerca semàntica que construeix i consulta incrustacions d'objectes per a trobar elements similars.",
|
||||||
|
"triggers": {
|
||||||
|
"label": "Activadors",
|
||||||
|
"description": "Accions i criteris coincidents per als desencadenants de cerca semàntica específics de la càmera.",
|
||||||
|
"friendly_name": {
|
||||||
|
"label": "Nom amistós",
|
||||||
|
"description": "Nom opcional amistós que es mostra a la interfície d'usuari per a aquest activador."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita aquest activador",
|
||||||
|
"description": "Activa o desactiva aquest activador de cerca semàntica."
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Tipus d'activador",
|
||||||
|
"description": "Tipus d'activador: «miniatures» (match contra imatge) o «descripció» (match contra text)."
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"label": "Contingut del disparador",
|
||||||
|
"description": "Frase de text o ID de miniatures per a coincidir amb els objectes rastrejats."
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"label": "Llindar d'activació",
|
||||||
|
"description": "Puntuació mínima de similitud (0-1) necessària per activar aquest activador."
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"label": "Accions d'activació",
|
||||||
|
"description": "Llista d'accions a executar quan coincideixi l'activador (notificació, sublabeletiqueta, atribut)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"label": "Instantànies",
|
||||||
|
"description": "Configuració per a les instantànies JPEG desades dels objectes seguits per a aquesta càmera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Instantànies habilitades",
|
||||||
|
"description": "Activa o desactiva el desament de les instantànies d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"clean_copy": {
|
||||||
|
"label": "Desa la còpia neta",
|
||||||
|
"description": "Desa una còpia neta no anotada de les instantànies a més de les anotades."
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"label": "Superposició de marca horària",
|
||||||
|
"description": "Superposa una marca horària a les instantànies desades."
|
||||||
|
},
|
||||||
|
"bounding_box": {
|
||||||
|
"label": "Superposició de la caixa contenidora",
|
||||||
|
"description": "Dibuixa caixes contenidores per als objectes seguits en les instantànies desades."
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"label": "Retalla la instantània",
|
||||||
|
"description": "Retalla les instantànies desades a la caixa contenidora de l'objecte detectat."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Zones que ha d'introduir un objecte perquè es desi una instantània."
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"label": "Alçada de la instantània",
|
||||||
|
"description": "Alçada (píxels) per a canviar la mida de les instantànies desades; deixeu-ho buit per a preservar la mida original."
|
||||||
|
},
|
||||||
|
"retain": {
|
||||||
|
"label": "Retenció de la instantània",
|
||||||
|
"description": "Paràmetres de retenció per a les instantànies desades, inclosos els dies predeterminats i les anul·lacions per objecte.",
|
||||||
|
"default": {
|
||||||
|
"label": "Retenció predeterminada",
|
||||||
|
"description": "Nombre predeterminat de dies per a retenir les instantànies."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Mode de retenció",
|
||||||
|
"description": "Mode de retenció: tot (desa tots els segments), moviment (desa els segments amb moviment), o actiuobobjectes (desa els segments amb objectes actius)."
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"label": "Retenció d'objectes",
|
||||||
|
"description": "Anul·lació per objecte per dies de retenció d'instantànies."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "Qualitat JPEG",
|
||||||
|
"description": "Qualitat del codi JPEG per a les instantànies desades (0-100)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp_style": {
|
||||||
|
"label": "Estil de la marca horària",
|
||||||
|
"description": "Opcions d'estilització per a marques de temps d'alimentació aplicades a enregistraments i instantànies.",
|
||||||
|
"position": {
|
||||||
|
"label": "Posició de la marca horària",
|
||||||
|
"description": "Posició de la marca horària a la imatge (tl/tr/bl/br)."
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"label": "Format de la marca horària",
|
||||||
|
"description": "Cadena de format de data i hora utilitzada per a marques horàries (codis de format de data i hora de Python)."
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"label": "Color de la marca horària",
|
||||||
|
"description": "Valors de color RGB per al text de la marca de temps (tots els valors 0-255).",
|
||||||
|
"red": {
|
||||||
|
"label": "Vermell",
|
||||||
|
"description": "Component vermell (0-255) per al color de la marca horària."
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"label": "Verd",
|
||||||
|
"description": "Component verd (0-255) per al color de la marca horària."
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"label": "Blau",
|
||||||
|
"description": "Component blau (0-255) per al color de la marca horària."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thickness": {
|
||||||
|
"label": "Gruix de la marca de temps",
|
||||||
|
"description": "Gruix de la línia del text de la marca de temps."
|
||||||
|
},
|
||||||
|
"effect": {
|
||||||
|
"label": "Efecte de marca horària",
|
||||||
|
"description": "Efecte visual per al text de la marca de temps (cap, sòlid, ombra)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"best_image_timeout": {
|
||||||
|
"label": "Temps d'espera de la millor imatge",
|
||||||
|
"description": "Quant de temps s'espera per a la imatge amb la puntuació de confiança més alta."
|
||||||
|
},
|
||||||
|
"mqtt": {
|
||||||
|
"label": "MQTT",
|
||||||
|
"description": "Configuració de la publicació d'imatges MQTT.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Envia la imatge",
|
||||||
|
"description": "Habilita la publicació d'instantànies d'imatges per a objectes als temes MQTT d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"label": "Afegeix una marca horària",
|
||||||
|
"description": "Superposa una marca horària a les imatges publicades a MQTT."
|
||||||
|
},
|
||||||
|
"bounding_box": {
|
||||||
|
"label": "Afegeix el quadre de delimitació",
|
||||||
|
"description": "Dibuixa caixes delimitadores en imatges publicades sobre MQTT."
|
||||||
|
},
|
||||||
|
"crop": {
|
||||||
|
"label": "Retalla la imatge",
|
||||||
|
"description": "Retalla les imatges publicades a MQTT a la caixa contenidora de l'objecte detectat."
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"label": "Alçada de la imatge",
|
||||||
|
"description": "Alçada (píxels) per a canviar la mida de les imatges publicades sobre MQTT."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Zones que ha d'introduir un objecte perquè es publiqui una imatge MQTT."
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"label": "Qualitat JPEG",
|
||||||
|
"description": "Qualitat JPEG per a les imatges publicades a MQTT (0-100)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"label": "Notificacions",
|
||||||
|
"description": "Configuració per a habilitar i controlar les notificacions d'aquesta càmera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita les notificacions",
|
||||||
|
"description": "Activa o desactiva les notificacions d'aquesta càmera."
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"label": "Correu electrònic de notificació",
|
||||||
|
"description": "Adreça de correu electrònic utilitzada per a notificacions push o requerides per determinats proveïdors de notificacions."
|
||||||
|
},
|
||||||
|
"cooldown": {
|
||||||
|
"label": "Període de reducció",
|
||||||
|
"description": "Retirada (segons) entre notificacions per evitar els destinataris de correu brossa."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de les notificacions originals",
|
||||||
|
"description": "Indica si les notificacions s'han activat en la configuració estàtica original."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"onvif": {
|
||||||
|
"label": "ONVIF",
|
||||||
|
"description": "Connexió ONVIF i configuració de seguiment automàtic PTZ per a aquesta càmera.",
|
||||||
|
"host": {
|
||||||
|
"label": "Servidor ONVIF",
|
||||||
|
"description": "Host (i esquema opcional) per al servei ONVIF per a aquesta càmera."
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"label": "Port ONVIF",
|
||||||
|
"description": "Número de port del servei ONVIF."
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"label": "Nom d'usuari ONVIF",
|
||||||
|
"description": "Nom d'usuari per a l'autenticació ONVIF; alguns dispositius requereixen l'usuari administrador per a ONVIF."
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Contrasenya ONVIF",
|
||||||
|
"description": "Contrasenya per a l'autenticació ONVIF."
|
||||||
|
},
|
||||||
|
"tls_insecure": {
|
||||||
|
"label": "Inhabilita la verificació TLS",
|
||||||
|
"description": "Omet la verificació TLS i desactiva l'autenticació de resum per a ONVIF (no segur; només s'utilitza en xarxes segures)."
|
||||||
|
},
|
||||||
|
"autotracking": {
|
||||||
|
"label": "Aeguiment automàtic",
|
||||||
|
"description": "Segueix automàticament els objectes en moviment i els manté centrats en el marc utilitzant els moviments de la càmera PTZ.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilita el seguiment automàtic",
|
||||||
|
"description": "Activa o desactiva el seguiment automàtic de la càmera PTZ dels objectes detectats."
|
||||||
|
},
|
||||||
|
"calibrate_on_startup": {
|
||||||
|
"label": "Calibra a l'inici",
|
||||||
|
"description": "Mesura les velocitats del motor PTZ a l'inici per millorar la precisió del seguiment. Frigate actualitzarà la configuració amb «movtion.weights» després del calibratge."
|
||||||
|
},
|
||||||
|
"zooming": {
|
||||||
|
"label": "Mode de zoom",
|
||||||
|
"description": "Comportament de zoom de control: desactivat (només pan/tilt), absolut (més compatible) o relatiu (pa/tilt/zoom concurrent)."
|
||||||
|
},
|
||||||
|
"zoom_factor": {
|
||||||
|
"label": "Factor de zoom",
|
||||||
|
"description": "Controla el nivell d'ampliació dels objectes rastrejats. Els valors més baixos mantenen més escena a la vista; els valors més alts s'apropen, però poden perdre el seguiment. Valors entre 0,1 i 0,75."
|
||||||
|
},
|
||||||
|
"track": {
|
||||||
|
"label": "Objectes rastrejats",
|
||||||
|
"description": "Llista de tipus d'objectes que haurien d'activar el seguiment automàtic."
|
||||||
|
},
|
||||||
|
"required_zones": {
|
||||||
|
"label": "Zones requerides",
|
||||||
|
"description": "Els objectes han d'entrar en una d'aquestes zones abans que comenci el seguiment automàtic."
|
||||||
|
},
|
||||||
|
"return_preset": {
|
||||||
|
"label": "Retorna la predefinició",
|
||||||
|
"description": "Nom predefinit ONVIF configurat al microprogramari de la càmera per tornar després de finalitzar el seguiment."
|
||||||
|
},
|
||||||
|
"timeout": {
|
||||||
|
"label": "Temps d'espera de retorn",
|
||||||
|
"description": "Espereu tants segons després de perdre el seguiment abans de tornar la càmera a la posició preestablerta."
|
||||||
|
},
|
||||||
|
"movement_weights": {
|
||||||
|
"label": "Pes del moviment",
|
||||||
|
"description": "Valors de calibratge generats automàticament pel calibratge de la càmera. No modifiquis manualment."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat de la pista automàtica original",
|
||||||
|
"description": "Camp intern per a fer el seguiment de si s'ha habilitat el seguiment automàtic a la configuració."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignore_time_mismatch": {
|
||||||
|
"label": "Ignora el desajust de temps",
|
||||||
|
"description": "Ignora les diferències de sincronització de temps entre càmera i servidor Frigate per a la comunicació ONVIF."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Tipus de càmera",
|
||||||
|
"description": "Tipus de càmera"
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"label": "Interfície d'usuari de la càmera",
|
||||||
|
"description": "Mostra l'ordre i la visibilitat d'aquesta càmera a la interfície d'usuari. La comanda afecta el tauler predeterminat. Per a un control més granular, utilitzeu grups de càmera.",
|
||||||
|
"order": {
|
||||||
|
"label": "Ordre de la interfície",
|
||||||
|
"description": "Ordre numèric utilitzat per ordenar la càmera a la interfície d'usuari (taulell de control i llistes per defecte); els nombres més grans apareixen més tard."
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"label": "Mostra a l'interfície d'usuari",
|
||||||
|
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de la Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webui_url": {
|
||||||
|
"label": "URL de la càmera",
|
||||||
|
"description": "URL per visitar la càmera directament des de la pàgina del sistema"
|
||||||
|
},
|
||||||
|
"zones": {
|
||||||
|
"label": "Zones",
|
||||||
|
"description": "Les zones permeten definir una àrea específica del marc perquè pugueu determinar si un objecte es troba dins d'una àrea determinada.",
|
||||||
|
"friendly_name": {
|
||||||
|
"label": "Nom de la zona",
|
||||||
|
"description": "Un nom fàcil d'utilitzar per a la zona, que es mostra a la interfície d'usuari de la fragata. Si no s'estableix, s'utilitzarà una versió amb format del nom de la zona."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilitat",
|
||||||
|
"description": "Activa o desactiva aquesta zona. Les zones inhabilitades s'ignoren en temps d'execució."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Feu un seguiment de l'estat original de la zona."
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filtres de zona",
|
||||||
|
"description": "Filtres que s'aplicaran als objectes d'aquesta zona. S'utilitza per reduir falsos positius o restringir quins objectes es consideren presents a la zona.",
|
||||||
|
"min_area": {
|
||||||
|
"label": "Àrea mínima de l'objecte",
|
||||||
|
"description": "Es requereix una àrea de caixa contenidora mínima (píxels o percentatge) per a aquest tipus d'objecte. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
|
||||||
|
},
|
||||||
|
"max_area": {
|
||||||
|
"label": "Àrea màxima de l'objecte",
|
||||||
|
"description": "Es permet l'àrea màxima de la caixa contenidora (píxels o percentatge) per a aquest tipus d'objecte. Pot ser píxels (int) o percentatge (float entre 0,000001 i 0.99)."
|
||||||
|
},
|
||||||
|
"min_ratio": {
|
||||||
|
"label": "Relació mínima d'aspecte",
|
||||||
|
"description": "Relació mínima d'amplada/alçada requerida per a la casella contenidora a qualificar."
|
||||||
|
},
|
||||||
|
"max_ratio": {
|
||||||
|
"label": "Relació màxima d'aspecte",
|
||||||
|
"description": "Es permet la relació màxima d'amplada/alçada per a la casella contenidora a qualificar."
|
||||||
|
},
|
||||||
|
"threshold": {
|
||||||
|
"label": "Llindar de confiança",
|
||||||
|
"description": "Es requereix un llindar de confiança mitjà per a la detecció perquè l'objecte es consideri un veritable positiu."
|
||||||
|
},
|
||||||
|
"min_score": {
|
||||||
|
"label": "Confiança mínima",
|
||||||
|
"description": "Es requereix una confiança mínima de detecció d'un sol fotograma per a comptar l'objecte."
|
||||||
|
},
|
||||||
|
"mask": {
|
||||||
|
"label": "Màscara de filtre",
|
||||||
|
"description": "Coordenades de polígon que defineixen on s'aplica aquest filtre dins del marc."
|
||||||
|
},
|
||||||
|
"raw_mask": {
|
||||||
|
"label": "Màscara en brut"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"description": "Llista de tipus d'objectes (des del mapa d'etiquetes) que poden activar aquesta zona. Pot ser una cadena o una llista de cadenes. Si està buit, es consideraran tots els objectes.",
|
||||||
|
"label": "Objectes d'activació"
|
||||||
|
},
|
||||||
|
"coordinates": {
|
||||||
|
"label": "Coordenades",
|
||||||
|
"description": "Coordenades de polígon que defineixen l'àrea de zona. Pot ser una cadena separada per comes o una llista de cadenes de coordenades. Les coordenades han de ser relatives (0-1) o absolutes (antic)."
|
||||||
|
},
|
||||||
|
"distances": {
|
||||||
|
"label": "Distàncies del món real",
|
||||||
|
"description": "Distàncies opcionals del món real per a cada costat del quadrilàter de la zona, utilitzades per a càlculs de velocitat o distància. Si s'estableix, ha de tenir exactament 4 valors."
|
||||||
|
},
|
||||||
|
"inertia": {
|
||||||
|
"label": "Fotogrames d'inèrcia",
|
||||||
|
"description": "Nombre de fotogrames consecutius que s'ha de detectar un objecte a la zona abans de considerar-lo present. Ajuda a filtrar les deteccions transitòries."
|
||||||
|
},
|
||||||
|
"loitering_time": {
|
||||||
|
"label": "Segons flotants",
|
||||||
|
"description": "Nombre de segons que un objecte ha de romandre a la zona a considerar com a errant. Establiu-ho a 0 per a desactivar la detecció de la itinerància."
|
||||||
|
},
|
||||||
|
"speed_threshold": {
|
||||||
|
"label": "Velocitat mínima",
|
||||||
|
"description": "Velocitat mínima (en unitats del món real si s'estableixen distàncies) necessària perquè un objecte es consideri present a la zona. S'utilitza per a activadors de zona basats en velocitat."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Estat original de la càmera",
|
||||||
|
"description": "Feu un seguiment de l'estat original de la càmera."
|
||||||
|
}
|
||||||
|
}
|
||||||
2192
web/public/locales/ca/config/global.json
Normal file
2192
web/public/locales/ca/config/global.json
Normal file
File diff suppressed because it is too large
Load Diff
73
web/public/locales/ca/config/groups.json
Normal file
73
web/public/locales/ca/config/groups.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"global": {
|
||||||
|
"detection": "Detecció global",
|
||||||
|
"sensitivity": "Sensibilitat global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"detection": "Detecció",
|
||||||
|
"sensitivity": "Sensibilitat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp_style": {
|
||||||
|
"global": {
|
||||||
|
"appearance": "Aparença global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"appearance": "Aparença"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"global": {
|
||||||
|
"sensitivity": "Sensibilitat global",
|
||||||
|
"algorithm": "Algorisme global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"sensitivity": "Sensibilitat",
|
||||||
|
"algorithm": "Algorisme"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"global": {
|
||||||
|
"display": "Visualització global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"display": "Mostra"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"global": {
|
||||||
|
"resolution": "Resolució global",
|
||||||
|
"tracking": "Seguiment global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"resolution": "Resolució",
|
||||||
|
"tracking": "Seguiment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"global": {
|
||||||
|
"tracking": "Seguiment global",
|
||||||
|
"filtering": "Filtratge global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"tracking": "Seguiment",
|
||||||
|
"filtering": "Filtra"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"global": {
|
||||||
|
"retention": "Retenció global",
|
||||||
|
"events": "Esdeveniments globals"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"retention": "Retenció",
|
||||||
|
"events": "Esdeveniment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"cameras": {
|
||||||
|
"cameraFfmpeg": "Arguments específics del FFmpeg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
web/public/locales/ca/config/validation.json
Normal file
32
web/public/locales/ca/config/validation.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"minimum": "Ha de ser com a mínim {{limit}}",
|
||||||
|
"maximum": "Ha de ser com a màxim {{limit}}",
|
||||||
|
"exclusiveMinimum": "Ha de ser més gran que {{limit}}",
|
||||||
|
"exclusiveMaximum": "Ha de ser inferior a {{limit}}",
|
||||||
|
"minLength": "Ha de tenir com a mínim {{limit}} caràcters",
|
||||||
|
"maxLength": "Ha de tenir com a màxim {{limit}} caràcters",
|
||||||
|
"minItems": "Ha de tenir com a mínim {{limit}} elements",
|
||||||
|
"maxItems": "Ha de tenir com a màxim {{limit}} elements",
|
||||||
|
"pattern": "Format no vàlid",
|
||||||
|
"required": "Aquest camp és obligatori",
|
||||||
|
"type": "Tipus de valor no vàlid",
|
||||||
|
"enum": "Ha de ser un dels valors permesos",
|
||||||
|
"const": "El valor no coincideix amb la constant esperada",
|
||||||
|
"uniqueItems": "Tots els elements han de ser únics",
|
||||||
|
"format": "Format no vàlid",
|
||||||
|
"additionalProperties": "No es permet la propietat desconeguda",
|
||||||
|
"oneOf": "Ha de coincidir exactament amb un dels esquemes permesos",
|
||||||
|
"anyOf": "Ha de coincidir almenys amb un dels esquemes permesos",
|
||||||
|
"proxy": {
|
||||||
|
"header_map": {
|
||||||
|
"roleHeaderRequired": "Es requereix la capçalera del rol quan es configuren els mapes de rols."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": {
|
||||||
|
"rolesUnique": "Cada rol només es pot assignar a un flux d'entrada.",
|
||||||
|
"detectRequired": "Almenys un flux d'entrada ha de tenir assignat el rol «detecta».",
|
||||||
|
"hwaccelDetectOnly": "Només el flux d'entrada amb el rol detect pot definir arguments d'acceleració del maquinari."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"surfboard": "Taula de surf",
|
"surfboard": "Taula de surf",
|
||||||
"tennis_racket": "Raqueta de tenis",
|
"tennis_racket": "Raqueta de tenis",
|
||||||
"bottle": "Ampolla",
|
"bottle": "Ampolla",
|
||||||
"plate": "Placa",
|
"plate": "Matrícula",
|
||||||
"wine_glass": "Got de vi",
|
"wine_glass": "Got de vi",
|
||||||
"cup": "Copa",
|
"cup": "Copa",
|
||||||
"fork": "Forquilla",
|
"fork": "Forquilla",
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}",
|
"deleteImageFailed": "No s'ha pogut suprimir: {{errorMessage}}",
|
||||||
"deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}",
|
"deleteCategoryFailed": "No s'ha pogut suprimir la classe: {{errorMessage}}",
|
||||||
"categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}",
|
"categorizeFailed": "No s'ha pogut categoritzar la imatge: {{errorMessage}}",
|
||||||
"trainingFailed": "Ha fallat l'entrenament del model. Comproveu els registres de fragata per a més detalls.",
|
"trainingFailed": "Ha fallat l'entrenament del model. Comproveu els registres de Frigate per a més detalls.",
|
||||||
"deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}",
|
"deleteModelFailed": "No s'ha pogut suprimir el model: {{errorMessage}}",
|
||||||
"updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}",
|
"updateModelFailed": "No s'ha pogut actualitzar el model: {{errorMessage}}",
|
||||||
"renameCategoryFailed": "No s'ha pogut canviar el nom de la classe: {{errorMessage}}",
|
"renameCategoryFailed": "No s'ha pogut canviar el nom de la classe: {{errorMessage}}",
|
||||||
|
|||||||
@ -63,5 +63,25 @@
|
|||||||
"normalActivity": "Normal",
|
"normalActivity": "Normal",
|
||||||
"needsReview": "Necessita revisió",
|
"needsReview": "Necessita revisió",
|
||||||
"securityConcern": "Preocupació per la seguretat",
|
"securityConcern": "Preocupació per la seguretat",
|
||||||
"select_all": "Tots"
|
"select_all": "Tots",
|
||||||
|
"motionSearch": {
|
||||||
|
"menuItem": "Cerca de moviment",
|
||||||
|
"openMenu": "Opcions de la càmera"
|
||||||
|
},
|
||||||
|
"motionPreviews": {
|
||||||
|
"menuItem": "Visualitza les vistes prèvies del moviment",
|
||||||
|
"title": "Vista prèvia del moviment: {{camera}}",
|
||||||
|
"mobileSettingsTitle": "Configuració de la vista prèvia del moviment",
|
||||||
|
"mobileSettingsDesc": "Ajusteu la velocitat de reproducció i l'enfosquiment, i trieu una data per a revisar clips només en moviment.",
|
||||||
|
"dim": "Atenuar",
|
||||||
|
"dimAria": "Ajusta la intensitat de l'enfosquiment",
|
||||||
|
"dimDesc": "Incrementa l'enfosquiment per augmentar la visibilitat de l'àrea de moviment.",
|
||||||
|
"speed": "Velocitat",
|
||||||
|
"speedAria": "Selecciona la velocitat de reproducció de la vista prèvia",
|
||||||
|
"speedDesc": "Trieu la rapidesa amb què es reprodueixen els clips de vista prèvia.",
|
||||||
|
"back": "Enrere",
|
||||||
|
"empty": "No hi ha cap vista prèvia disponible",
|
||||||
|
"noPreview": "Vista prèvia no disponible",
|
||||||
|
"seekAria": "Cerca el reproductor {{camera}} a {{time}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -234,6 +234,10 @@
|
|||||||
"downloadCleanSnapshot": {
|
"downloadCleanSnapshot": {
|
||||||
"label": "Descarrega la instantània neta",
|
"label": "Descarrega la instantània neta",
|
||||||
"aria": "Descarrega la instantània neta"
|
"aria": "Descarrega la instantània neta"
|
||||||
|
},
|
||||||
|
"debugReplay": {
|
||||||
|
"label": "Depura la repetició",
|
||||||
|
"aria": "Mostra aquest objecte rastrejat a la vista de reproducció de depuració"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"noTrackedObjects": "No s'han trobat objectes rastrejats",
|
"noTrackedObjects": "No s'han trobat objectes rastrejats",
|
||||||
@ -285,7 +289,7 @@
|
|||||||
"title": "Configuració d'anotacions",
|
"title": "Configuració d'anotacions",
|
||||||
"showAllZones": {
|
"showAllZones": {
|
||||||
"title": "Mostra totes les Zones",
|
"title": "Mostra totes les Zones",
|
||||||
"desc": "Mostra sempre les zones amb marcs on els objectes hagin entrat a la zona."
|
"desc": "Mostra sempre les zones amb fotogrames on els objectes hagin entrat a la zona."
|
||||||
},
|
},
|
||||||
"offset": {
|
"offset": {
|
||||||
"label": "Òfset d'Anotació",
|
"label": "Òfset d'Anotació",
|
||||||
|
|||||||
@ -11,13 +11,27 @@
|
|||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"error": {
|
"error": {
|
||||||
"renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}"
|
"renameExportFailed": "Error al canviar el nom de l’exportació: {{errorMessage}}",
|
||||||
|
"assignCaseFailed": "No s'ha pogut actualitzar l'assignació de cas:{{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"shareExport": "Comparteix l'exportació",
|
"shareExport": "Comparteix l'exportació",
|
||||||
"downloadVideo": "Baixa el vídeo",
|
"downloadVideo": "Baixa el vídeo",
|
||||||
"editName": "Edita el nom",
|
"editName": "Edita el nom",
|
||||||
"deleteExport": "Suprimeix l'exportació"
|
"deleteExport": "Suprimeix l'exportació",
|
||||||
|
"assignToCase": "Afegeix al cas"
|
||||||
|
},
|
||||||
|
"headings": {
|
||||||
|
"cases": "Casos",
|
||||||
|
"uncategorizedExports": "Exportacions sense categoria"
|
||||||
|
},
|
||||||
|
"caseDialog": {
|
||||||
|
"title": "Afegeix al cas",
|
||||||
|
"description": "Trieu un cas existent o creeu-ne un de nou.",
|
||||||
|
"selectLabel": "Cas",
|
||||||
|
"newCaseOption": "Crea un cas nou",
|
||||||
|
"nameLabel": "Nom del cas",
|
||||||
|
"descriptionLabel": "Descripció"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,17 +7,20 @@
|
|||||||
"authentication": "Configuració d'autenticació - Frigate",
|
"authentication": "Configuració d'autenticació - Frigate",
|
||||||
"camera": "Paràmetres de càmera - Frigate",
|
"camera": "Paràmetres de càmera - Frigate",
|
||||||
"masksAndZones": "Editor de màscares i zones - Frigate",
|
"masksAndZones": "Editor de màscares i zones - Frigate",
|
||||||
"general": "Configuració de la interfície d'usuari - Fragata",
|
"general": "Configuració del perfil - Frigate",
|
||||||
"frigatePlus": "Paràmetres de Frigate+ - Frigate",
|
"frigatePlus": "Paràmetres de Frigate+ - Frigate",
|
||||||
"notifications": "Paràmetres de notificació - Frigate",
|
"notifications": "Paràmetres de notificació - Frigate",
|
||||||
"cameraManagement": "Gestionar càmeres - Frigate",
|
"cameraManagement": "Gestionar càmeres - Frigate",
|
||||||
"cameraReview": "Configuració Revisió de Càmeres - Frigate"
|
"cameraReview": "Configuració Revisió de Càmeres - Frigate",
|
||||||
|
"globalConfig": "Configuració global - Frigate",
|
||||||
|
"cameraConfig": "Configuració de la càmera - Frigate",
|
||||||
|
"maintenance": "Manteniment - Frigate"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"ui": "Interfície d'usuari",
|
"ui": "Interfície d'usuari",
|
||||||
"cameras": "Paràmetres de la càmera",
|
"cameras": "Paràmetres de la càmera",
|
||||||
"masksAndZones": "Màscares / Zones",
|
"masksAndZones": "Màscares / Zones",
|
||||||
"motionTuner": "Ajust de detecció de moviment",
|
"motionTuner": "Afinador de moviment",
|
||||||
"users": "Usuaris",
|
"users": "Usuaris",
|
||||||
"notifications": "Notificacions",
|
"notifications": "Notificacions",
|
||||||
"debug": "Depuració",
|
"debug": "Depuració",
|
||||||
@ -26,7 +29,64 @@
|
|||||||
"triggers": "Disparadors",
|
"triggers": "Disparadors",
|
||||||
"cameraManagement": "Gestió",
|
"cameraManagement": "Gestió",
|
||||||
"cameraReview": "Revisió",
|
"cameraReview": "Revisió",
|
||||||
"roles": "Rols"
|
"roles": "Rols",
|
||||||
|
"general": "General",
|
||||||
|
"globalConfig": "Configuració global",
|
||||||
|
"system": "Sistema",
|
||||||
|
"integrations": "Integracions",
|
||||||
|
"profileSettings": "Configuració del perfil",
|
||||||
|
"globalDetect": "Detecció d'objectes",
|
||||||
|
"globalRecording": "Enregistrament",
|
||||||
|
"globalSnapshots": "Instantànies",
|
||||||
|
"globalFfmpeg": "FFmpeg",
|
||||||
|
"globalMotion": "Detecció de moviment",
|
||||||
|
"globalObjects": "Objectes",
|
||||||
|
"globalReview": "Revisió",
|
||||||
|
"globalAudioEvents": "Esdeveniments d'àudio",
|
||||||
|
"globalLivePlayback": "Reproducció en directe",
|
||||||
|
"globalTimestampStyle": "Estil de la marca horària",
|
||||||
|
"systemDatabase": "Base de dades",
|
||||||
|
"systemTls": "TLS",
|
||||||
|
"systemAuthentication": "Autenticació",
|
||||||
|
"systemNetworking": "Xarxa",
|
||||||
|
"systemProxy": "Proxy",
|
||||||
|
"systemUi": "UI",
|
||||||
|
"systemLogging": "Registre",
|
||||||
|
"systemEnvironmentVariables": "Variables d'entorn",
|
||||||
|
"systemTelemetry": "Telemetria",
|
||||||
|
"systemBirdseye": "Birdseye",
|
||||||
|
"systemFfmpeg": "FFmpeg",
|
||||||
|
"systemDetectorHardware": "Hardware del detector",
|
||||||
|
"systemDetectionModel": "Model de detecció",
|
||||||
|
"systemMqtt": "MQTT",
|
||||||
|
"integrationSemanticSearch": "Cerca semàntica",
|
||||||
|
"integrationGenerativeAi": "IA generativa",
|
||||||
|
"integrationFaceRecognition": "Reconeixement de cares",
|
||||||
|
"integrationLpr": "Reconeixement de la matrícula",
|
||||||
|
"integrationObjectClassification": "Classificació de l'objecte",
|
||||||
|
"integrationAudioTranscription": "Transcripció d'àudio",
|
||||||
|
"cameraDetect": "Detecció d'objectes",
|
||||||
|
"cameraFfmpeg": "FFmpeg",
|
||||||
|
"cameraRecording": "Enregistrament",
|
||||||
|
"cameraSnapshots": "Instantànies",
|
||||||
|
"cameraMotion": "Detecció de moviment",
|
||||||
|
"cameraObjects": "Objectes",
|
||||||
|
"cameraConfigReview": "Revisió",
|
||||||
|
"cameraAudioEvents": "Esdeveniments d'àudio",
|
||||||
|
"cameraAudioTranscription": "Transcripció d'àudio",
|
||||||
|
"cameraNotifications": "Notificacions",
|
||||||
|
"cameraLivePlayback": "Reproducció en directe",
|
||||||
|
"cameraBirdseye": "Birdseye",
|
||||||
|
"cameraFaceRecognition": "Reconeixement de cares",
|
||||||
|
"cameraLpr": "Reconeixement de la matrícula",
|
||||||
|
"cameraMqttConfig": "MQTT",
|
||||||
|
"cameraOnvif": "ONVIF",
|
||||||
|
"cameraUi": "UI de la càmera",
|
||||||
|
"cameraTimestampStyle": "Estil de la marca horària",
|
||||||
|
"cameraMqtt": "Càmera MQTT",
|
||||||
|
"maintenance": "Manteniment",
|
||||||
|
"mediaSync": "Sincronització multimèdia",
|
||||||
|
"regionGrid": "Quadrícula de la regió"
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"unsavedChanges": {
|
"unsavedChanges": {
|
||||||
@ -39,7 +99,7 @@
|
|||||||
"noCamera": "Cap càmera"
|
"noCamera": "Cap càmera"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"title": "Paràmetres de la interfície d'usuari",
|
"title": "Configuració del perfil",
|
||||||
"liveDashboard": {
|
"liveDashboard": {
|
||||||
"title": "Panell en directe",
|
"title": "Panell en directe",
|
||||||
"automaticLiveView": {
|
"automaticLiveView": {
|
||||||
@ -205,6 +265,10 @@
|
|||||||
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
"clickDrawPolygon": "Fes click per a dibuixar un polígon a la imatge.",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": "S'ha desat la zona ({{zoneName}})."
|
"success": "S'ha desat la zona ({{zoneName}})."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"title": "Habilitat",
|
||||||
|
"description": "Si aquesta zona està activa i activada al fitxer de configuració. Si està desactivat, no pot ser habilitat per MQTT. Les zones inhabilitades s'ignoren en temps d'execució."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
@ -237,6 +301,12 @@
|
|||||||
"title": "{{polygonName}} s'ha desat.",
|
"title": "{{polygonName}} s'ha desat.",
|
||||||
"noName": "La màscara de moviment ha estat desada."
|
"noName": "La màscara de moviment ha estat desada."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"defaultName": "Màscara de moviment {{number}}",
|
||||||
|
"name": {
|
||||||
|
"title": "Nom",
|
||||||
|
"description": "Un nom opcional per a aquesta màscara de moviment.",
|
||||||
|
"placeholder": "Introduïu un nom..."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"objectMasks": {
|
"objectMasks": {
|
||||||
@ -263,11 +333,16 @@
|
|||||||
"noName": "La màscara d'objectes ha estat desada."
|
"noName": "La màscara d'objectes ha estat desada."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"context": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació."
|
"context": "Les màscares de filtratge d’objectes s’utilitzen per descartar falsos positius d’un tipus d’objecte concret segons la seva ubicació.",
|
||||||
|
"name": {
|
||||||
|
"title": "Nom",
|
||||||
|
"description": "Un nom opcional per a aquesta màscara d'objecte.",
|
||||||
|
"placeholder": "Introduïu un nom..."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"restart_required": "Reinici necessari (canvi de màscares o zones)",
|
"restart_required": "Reinici necessari (canvi de màscares o zones)",
|
||||||
"motionMaskLabel": "Màscara de moviment {{number}}",
|
"motionMaskLabel": "Màscara de moviment {{number}}",
|
||||||
"objectMaskLabel": "Màscara d'objecte {{number}} ({{label}})",
|
"objectMaskLabel": "Màscara d'objecte {{number}}",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": {
|
"success": {
|
||||||
"copyCoordinates": "S'han copiat les coordenades per a {{polyName}} al porta-retalls."
|
"copyCoordinates": "S'han copiat les coordenades per a {{polyName}} al porta-retalls."
|
||||||
@ -275,6 +350,13 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"copyCoordinatesFailed": "No s'han pogut copiar les coordenades al porta-retalls."
|
"copyCoordinatesFailed": "No s'han pogut copiar les coordenades al porta-retalls."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"disabledInConfig": "L'element està desactivat al fitxer de configuració",
|
||||||
|
"masks": {
|
||||||
|
"enabled": {
|
||||||
|
"title": "Habilitat",
|
||||||
|
"description": "Si aquesta màscara està activada al fitxer de configuració. Si està desactivat, no pot ser habilitat per MQTT. Les màscares desactivades s'ignoren en temps d'execució."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
@ -644,7 +726,14 @@
|
|||||||
"error": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}",
|
"error": "No s'han pogut guardar els canvis de configuració: {{errorMessage}}",
|
||||||
"success": "Els paràmetres de Frigate+ han estat desats. Reincia Frigate per aplicar els canvis."
|
"success": "Els paràmetres de Frigate+ han estat desats. Reincia Frigate per aplicar els canvis."
|
||||||
},
|
},
|
||||||
"restart_required": "Es necessari un reinici (El model de Frigate+ ha cambiat)"
|
"restart_required": "Es necessari un reinici (El model de Frigate+ ha cambiat)",
|
||||||
|
"description": "Frigate+ és un servei de subscripció que proporciona accés a funcions i capacitats addicionals per a la vostra instància de Frigate, inclosa la capacitat d'utilitzar models de detecció d'objectes personalitzats entrenats en les vostres pròpies dades. Podeu gestionar la configuració del model Frigate+ aquí.",
|
||||||
|
"cardTitles": {
|
||||||
|
"api": "API",
|
||||||
|
"currentModel": "Model actual",
|
||||||
|
"otherModels": "Altres models",
|
||||||
|
"configuration": "Configuració"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"enrichments": {
|
"enrichments": {
|
||||||
"semanticSearch": {
|
"semanticSearch": {
|
||||||
@ -665,7 +754,7 @@
|
|||||||
"success": "La reindexació ha començat amb èxit.",
|
"success": "La reindexació ha començat amb èxit.",
|
||||||
"label": "Reindexar ara",
|
"label": "Reindexar ara",
|
||||||
"confirmTitle": "Confirmar la reindexació",
|
"confirmTitle": "Confirmar la reindexació",
|
||||||
"desc": "La reindexació regenerarà les incrustacions (embeddings) de tots els objectes seguits. Aquest procés s’executa en segon pla i pot arribar a saturar la CPU, així com trigar una bona estona depenent del nombre d’objectes seguits que tinguis.",
|
"desc": "La reindexació regenerarà les incrustacions per a tots els objectes rastrejats. Aquest procés s'executa en segon pla i pot treure el màxim de la CPU i prendre una quantitat de temps raonable depenent del nombre d'objectes rastrejats que tingueu.",
|
||||||
"confirmDesc": "Estàs segur que vols reindexar totes les incrustacions (embeddings) dels objectes seguits? Aquest procés s’executarà en segon pla, però pot arribar a saturar la CPU i trigar bastant temps. Pots seguir-ne el progrés a la pàgina d’Explora.",
|
"confirmDesc": "Estàs segur que vols reindexar totes les incrustacions (embeddings) dels objectes seguits? Aquest procés s’executarà en segon pla, però pot arribar a saturar la CPU i trigar bastant temps. Pots seguir-ne el progrés a la pàgina d’Explora.",
|
||||||
"alreadyInProgress": "La reindexació ja està en curs.",
|
"alreadyInProgress": "La reindexació ja està en curs.",
|
||||||
"error": "Error en iniciar la reindexació: {{errorMessage}}"
|
"error": "Error en iniciar la reindexació: {{errorMessage}}"
|
||||||
@ -1181,7 +1270,12 @@
|
|||||||
"backToSettings": "Torna a la configuració de la càmera",
|
"backToSettings": "Torna a la configuració de la càmera",
|
||||||
"streams": {
|
"streams": {
|
||||||
"title": "Habilita / Inhabilita les càmeres",
|
"title": "Habilita / Inhabilita les càmeres",
|
||||||
"desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>"
|
"desc": "Inhabilita temporalment una càmera fins que es reiniciï la fragata. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>",
|
||||||
|
"enableLabel": "Càmeres habilitades",
|
||||||
|
"enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no desactiva les retransmissions de go2rtc.</em>",
|
||||||
|
"disableLabel": "Càmeres inhabilitades",
|
||||||
|
"disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.",
|
||||||
|
"enableSuccess": "{{cameraName}} activat a la configuració. Reinicia Frigate per aplicar els canvis."
|
||||||
},
|
},
|
||||||
"cameraConfig": {
|
"cameraConfig": {
|
||||||
"add": "Afegeix una càmera",
|
"add": "Afegeix una càmera",
|
||||||
@ -1211,6 +1305,18 @@
|
|||||||
"toast": {
|
"toast": {
|
||||||
"success": "La càmera {{cameraName}} s'ha desat correctament"
|
"success": "La càmera {{cameraName}} s'ha desat correctament"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"deleteCamera": "Suprimeix la càmera",
|
||||||
|
"deleteCameraDialog": {
|
||||||
|
"title": "Suprimeix la càmera",
|
||||||
|
"description": "Suprimir una càmera eliminarà permanentment tots els enregistraments, els objectes rastrejats i la configuració d'aquesta càmera. Qualsevol flux go2rtc associat amb aquesta càmera encara pot haver de ser eliminat manualment.",
|
||||||
|
"selectPlaceholder": "Trieu la càmera...",
|
||||||
|
"confirmTitle": "N'estàs segur?",
|
||||||
|
"confirmWarning": "Suprimir <strong>{{cameraName}}</strong> no es pot desfer.",
|
||||||
|
"deleteExports": "Elimina també les exportacions d'aquesta càmera",
|
||||||
|
"confirmButton": "Suprimeix permanentment",
|
||||||
|
"success": "La càmera {{cameraName}} s'ha suprimit correctament",
|
||||||
|
"error": "No s'ha pogut suprimir la càmera {{cameraName}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cameraReview": {
|
"cameraReview": {
|
||||||
@ -1236,7 +1342,7 @@
|
|||||||
"selectDetectionsZones": "Selecció de zones per a les deteccions",
|
"selectDetectionsZones": "Selecció de zones per a les deteccions",
|
||||||
"limitDetections": "Limita les deteccions a zones específiques",
|
"limitDetections": "Limita les deteccions a zones específiques",
|
||||||
"toast": {
|
"toast": {
|
||||||
"success": "S'ha desat la configuració de la classificació de la revisió. Reinicia la fragata per aplicar canvis."
|
"success": "S'ha desat la configuració de la classificació de la revisió. Reinicia Frigate per aplicar canvis."
|
||||||
},
|
},
|
||||||
"unsavedChanges": "Paràmetres de classificació de revisions sense desar per {{camera}}",
|
"unsavedChanges": "Paràmetres de classificació de revisions sense desar per {{camera}}",
|
||||||
"objectAlertsTips": "Totes els objectes {{alertsLabels}} de {{cameraName}} es mostraran com avisos.",
|
"objectAlertsTips": "Totes els objectes {{alertsLabels}} de {{cameraName}} es mostraran com avisos.",
|
||||||
@ -1249,5 +1355,265 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Paràmetres de Revisió de la Càmera"
|
"title": "Paràmetres de Revisió de la Càmera"
|
||||||
}
|
},
|
||||||
|
"saveAllPreview": {
|
||||||
|
"title": "Canvis a desar",
|
||||||
|
"triggerLabel": "Revisa els canvis pendents",
|
||||||
|
"empty": "No hi ha canvis pendents.",
|
||||||
|
"scope": {
|
||||||
|
"label": "Àmbit",
|
||||||
|
"global": "Global",
|
||||||
|
"camera": "Càmara:{{cameraName}}"
|
||||||
|
},
|
||||||
|
"field": {
|
||||||
|
"label": "Camp"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"label": "Valor nou",
|
||||||
|
"reset": "Restableix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detectionModel": {
|
||||||
|
"plusActive": {
|
||||||
|
"title": "Gestió del model Frigate+",
|
||||||
|
"label": "Font del model actual",
|
||||||
|
"description": "Aquesta instància està executant un model Frigate+. Seleccioneu o canvieu el vostre model a la configuració de Frigate+.",
|
||||||
|
"goToFrigatePlus": "Ves a la configuració de Frigate+",
|
||||||
|
"showModelForm": "Configuració manual d'un model"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"maintenance": {
|
||||||
|
"title": "Manteniment",
|
||||||
|
"sync": {
|
||||||
|
"title": "Sincronització multimèdia",
|
||||||
|
"desc": "Frigate netejarà periòdicament els mitjans en un horari regular segons la configuració de la seva retenció. És normal veure alguns arxius orfes mentre corre Frigate. Utilitzeu aquesta característica per eliminar fitxers multimèdia orfes del disc que ja no estan referenciats a la base de dades.",
|
||||||
|
"started": "S'ha iniciat la sincronització del mitjà.",
|
||||||
|
"alreadyRunning": "Ja s'està executant una tasca de sincronització",
|
||||||
|
"error": "No s'ha pogut iniciar la sincronització",
|
||||||
|
"currentStatus": "Estat",
|
||||||
|
"jobId": "ID de la tasca",
|
||||||
|
"startTime": "Hora d'inici",
|
||||||
|
"endTime": "Hora final",
|
||||||
|
"statusLabel": "Estat",
|
||||||
|
"results": "Resultats",
|
||||||
|
"errorLabel": "Error",
|
||||||
|
"mediaTypes": "Tipus de suport",
|
||||||
|
"allMedia": "Tots els suports",
|
||||||
|
"dryRun": "Executa en sec",
|
||||||
|
"dryRunEnabled": "No s'eliminarà cap fitxer",
|
||||||
|
"dryRunDisabled": "S'eliminaran els fitxers",
|
||||||
|
"force": "Força",
|
||||||
|
"forceDesc": "Evita el llindar de seguretat i completa la sincronització fins i tot si més del 50% dels fitxers s'eliminarien.",
|
||||||
|
"running": "Sincronització en execució...",
|
||||||
|
"start": "Inicia la sincronització",
|
||||||
|
"inProgress": "La sincronització està en curs. Aquesta pàgina està desactivada.",
|
||||||
|
"status": {
|
||||||
|
"queued": "En cua",
|
||||||
|
"running": "En execució",
|
||||||
|
"completed": "Completat",
|
||||||
|
"failed": "Ha fallat",
|
||||||
|
"notRunning": "No s'està executant"
|
||||||
|
},
|
||||||
|
"resultsFields": {
|
||||||
|
"filesChecked": "Fitxers comprovats",
|
||||||
|
"orphansFound": "Orfes trobades",
|
||||||
|
"orphansDeleted": "Orfes eliminats",
|
||||||
|
"aborted": "Avortat. La supressió superaria el llindar de seguretat.",
|
||||||
|
"error": "Error",
|
||||||
|
"totals": "Totals"
|
||||||
|
},
|
||||||
|
"event_snapshots": "Instantànies de l'objecte rastrejat",
|
||||||
|
"event_thumbnails": "Miniatures d'objecte rastrejat",
|
||||||
|
"review_thumbnails": "Revisa les miniatures",
|
||||||
|
"previews": "Previsualitzacions",
|
||||||
|
"exports": "Exporta",
|
||||||
|
"recordings": "Enregistraments"
|
||||||
|
},
|
||||||
|
"regionGrid": {
|
||||||
|
"title": "Quadrícula de la regió",
|
||||||
|
"desc": "La quadrícula de regions és una optimització que aprèn on solen aparèixer objectes de diferents mides en el camp de visió de cada càmera. Frigate utilitza aquestes dades per detectar regions de mida eficient. La quadrícula es construeix automàticament amb el temps a partir de dades d'objectes rastrejats.",
|
||||||
|
"clear": "Neteja la quadrícula de la regió",
|
||||||
|
"clearConfirmTitle": "Neteja la quadrícula de la regió",
|
||||||
|
"clearConfirmDesc": "No es recomana netejar la quadrícula de la regió tret que hagi canviat recentment la mida del model del detector o hagi canviat la posició física de la càmera i tingui problemes de seguiment d'objectes. La quadrícula es reconstruirà automàticament amb el temps a mesura que els objectes siguin rastrejats. Es requereix un reinici de la fragata perquè els canvis tinguin efecte.",
|
||||||
|
"clearSuccess": "La quadrícula de la regió s'ha netejat correctament",
|
||||||
|
"clearError": "Ha fallat en netejar la graella de la regió",
|
||||||
|
"restartRequired": "Cal reiniciar per a que els canvis de la quadrícula de la regió tinguin efecte"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configForm": {
|
||||||
|
"global": {
|
||||||
|
"title": "Configuració global",
|
||||||
|
"description": "Aquestes opcions de configuració s'apliquen a totes les càmeres, llevat que se substitueixin en la configuració específica de la càmera."
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"title": "Configuració de la càmera",
|
||||||
|
"description": "Aquests paràmetres només s'apliquen a aquesta càmera i substitueixen els paràmetres globals."
|
||||||
|
},
|
||||||
|
"advancedSettingsCount": "Configuració avançada ({{count}})",
|
||||||
|
"advancedCount": "Avançat ({{count}})",
|
||||||
|
"showAdvanced": "Mostra la configuració avançada",
|
||||||
|
"tabs": {
|
||||||
|
"sharedDefaults": "Per defecte compartit",
|
||||||
|
"system": "Sistema",
|
||||||
|
"integrations": "Integracions"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"keyLabel": "Clau",
|
||||||
|
"valueLabel": "Valor",
|
||||||
|
"keyPlaceholder": "Nou valor",
|
||||||
|
"remove": "Elimina"
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"defaultOption": "Utilitza la zona horària del navegador"
|
||||||
|
},
|
||||||
|
"roleMap": {
|
||||||
|
"empty": "No hi ha assignacions de rols",
|
||||||
|
"roleLabel": "Rol",
|
||||||
|
"groupsLabel": "Grups",
|
||||||
|
"addMapping": "Afegeix un mapatge de rol",
|
||||||
|
"remove": "Elimina"
|
||||||
|
},
|
||||||
|
"ffmpegArgs": {
|
||||||
|
"preset": "Predefinit",
|
||||||
|
"manual": "Arguments manuals",
|
||||||
|
"inherit": "Hereta de la configuració de la càmera",
|
||||||
|
"selectPreset": "Selecció de valors predefinits",
|
||||||
|
"manualPlaceholder": "ntroduïu els arguments FFmpeg"
|
||||||
|
},
|
||||||
|
"cameraInputs": {
|
||||||
|
"itemTitle": "Flux {{index}}"
|
||||||
|
},
|
||||||
|
"restartRequiredField": "Reinicia requerit",
|
||||||
|
"restartRequiredFooter": "S'ha canviat la configuració - es requereix reiniciar",
|
||||||
|
"sections": {
|
||||||
|
"detect": "Detecció",
|
||||||
|
"record": "Enregistrament",
|
||||||
|
"snapshots": "Instantànies",
|
||||||
|
"motion": "Moviment",
|
||||||
|
"objects": "Objectes",
|
||||||
|
"review": "Revisió",
|
||||||
|
"audio": "Àudio",
|
||||||
|
"notifications": "Notificacions",
|
||||||
|
"live": "Vista en viu",
|
||||||
|
"timestamp_style": "Marques temporals",
|
||||||
|
"mqtt": "MQTT",
|
||||||
|
"database": "Base de dades",
|
||||||
|
"telemetry": "Telemetria",
|
||||||
|
"auth": "Autenticació",
|
||||||
|
"tls": "TLS",
|
||||||
|
"proxy": "Proxy",
|
||||||
|
"go2rtc": "go2rtc",
|
||||||
|
"ffmpeg": "FFmpeg",
|
||||||
|
"detectors": "Detectors",
|
||||||
|
"model": "Model",
|
||||||
|
"semantic_search": "Cerca semàntica",
|
||||||
|
"genai": "GenAI",
|
||||||
|
"face_recognition": "Reconeixement de cares",
|
||||||
|
"lpr": "Reconeixement de matrícules",
|
||||||
|
"birdseye": "Birdseye"
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"title": "Configuració de detecció"
|
||||||
|
},
|
||||||
|
"detectors": {
|
||||||
|
"title": "Configuració del detector",
|
||||||
|
"singleType": "Només es permet un detector {{type}}.",
|
||||||
|
"keyRequired": "Es requereix el nom del detector.",
|
||||||
|
"keyDuplicate": "El nom del detector ja existeix.",
|
||||||
|
"noSchema": "No hi ha esquemes de detector disponibles.",
|
||||||
|
"none": "No s'ha configurat cap instància de detector.",
|
||||||
|
"add": "Afegeix un detector"
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"title": "Configuració de l'enregistrament"
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"title": "Configuració de la instantània"
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"title": "Configuració del moviment"
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"title": "Configuració de l'objecte"
|
||||||
|
},
|
||||||
|
"audioLabels": {
|
||||||
|
"summary": "{{count}} etiquetes d'àudio seleccionades",
|
||||||
|
"empty": "No hi ha etiquetes d'àudio disponibles"
|
||||||
|
},
|
||||||
|
"objectLabels": {
|
||||||
|
"summary": "{{count}} tipus d'objectes seleccionats",
|
||||||
|
"empty": "No hi ha cap etiqueta d'objecte disponible"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"objectFieldLabel": "{{field}} per {{label}}"
|
||||||
|
},
|
||||||
|
"zoneNames": {
|
||||||
|
"summary": "{{count}} seleccionats",
|
||||||
|
"empty": "No hi ha zones disponibles"
|
||||||
|
},
|
||||||
|
"inputRoles": {
|
||||||
|
"summary": "{{count}} rols seleccionats",
|
||||||
|
"empty": "No hi ha cap rol disponible",
|
||||||
|
"options": {
|
||||||
|
"detect": "Detecta",
|
||||||
|
"record": "Enregistrament",
|
||||||
|
"audio": "Àudio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"title": "Configuració de la revisió"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"title": "Configuració de l'àudio"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"title": "Configuració de notificacions"
|
||||||
|
},
|
||||||
|
"live": {
|
||||||
|
"title": "Configuració de la vista en viu"
|
||||||
|
},
|
||||||
|
"timestamp_style": {
|
||||||
|
"title": "Configuració de la marca horària"
|
||||||
|
},
|
||||||
|
"searchPlaceholder": "Cerca..."
|
||||||
|
},
|
||||||
|
"globalConfig": {
|
||||||
|
"title": "Configuració global",
|
||||||
|
"description": "Configura la configuració global que s'aplica a totes les càmeres llevat que se sobreescriti.",
|
||||||
|
"toast": {
|
||||||
|
"success": "La configuració global s'ha desat correctament",
|
||||||
|
"error": "No s'ha pogut desar la configuració global",
|
||||||
|
"validationError": "Ha fallat la validació"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cameraConfig": {
|
||||||
|
"title": "Configuració de la càmera",
|
||||||
|
"description": "Configura la configuració per a les càmeres individuals. La configuració substitueix els valors predeterminats globals.",
|
||||||
|
"overriddenBadge": "Sobreescrit",
|
||||||
|
"resetToGlobal": "Restableix a global",
|
||||||
|
"toast": {
|
||||||
|
"success": "La configuració de la càmera s'ha desat correctament",
|
||||||
|
"error": "Ha fallat en desar la configuració de la càmera"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"success": "La configuració s'ha desat correctament",
|
||||||
|
"successRestartRequired": "La configuració s'ha desat correctament. Reinicia Frigate per aplicar els canvis.",
|
||||||
|
"error": "No s'ha pogut desar la configuració",
|
||||||
|
"validationError": "Ha fallat la validació: {{message}}",
|
||||||
|
"resetSuccess": "Restableix als valors predeterminats globals",
|
||||||
|
"resetError": "No s'ha pogut restablir la configuració",
|
||||||
|
"saveAllSuccess_one": "S'ha desat la secció {{count}} correctament.",
|
||||||
|
"saveAllSuccess_many": "Totes les {{count}} seccions s'han desat correctament.",
|
||||||
|
"saveAllSuccess_other": "Totes les {{count}} seccions s'han desat correctament.",
|
||||||
|
"saveAllPartial_one": "{{successCount}} de la secció {{totalCount}} desada. {{failCount}} ha fallat.",
|
||||||
|
"saveAllPartial_many": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
|
||||||
|
"saveAllPartial_other": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
|
||||||
|
"saveAllFailure": "Ha fallat en desar totes les seccions.",
|
||||||
|
"applied": "La configuració s'ha aplicat correctament"
|
||||||
|
},
|
||||||
|
"unsavedChanges": "Teniu canvis sense desar",
|
||||||
|
"confirmReset": "Confirma el restabliment",
|
||||||
|
"resetToDefaultDescription": "Això restablirà tots els paràmetres d'aquesta secció als seus valors predeterminats. Aquesta acció no es pot desfer.",
|
||||||
|
"resetToGlobalDescription": "Això restablirà la configuració d'aquesta secció als valors predeterminats globals. Aquesta acció no es pot desfer."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"frigate": "Registres de Frigate - Frigate",
|
"frigate": "Registres de Frigate - Frigate",
|
||||||
"go2rtc": "Registres de Go2RTC - Frigate",
|
"go2rtc": "Registres de Go2RTC - Frigate",
|
||||||
"nginx": "Registres de Nginix - Frigate"
|
"nginx": "Registres de Nginix - Frigate",
|
||||||
|
"websocket": "Registres de missatges - Frigate"
|
||||||
},
|
},
|
||||||
"enrichments": "Estadístiques complementàries - Frigate"
|
"enrichments": "Estadístiques complementàries - Frigate"
|
||||||
},
|
},
|
||||||
@ -33,6 +34,32 @@
|
|||||||
"fetchingLogsFailed": "Error al obtenir els registres: {{errorMessage}}",
|
"fetchingLogsFailed": "Error al obtenir els registres: {{errorMessage}}",
|
||||||
"whileStreamingLogs": "Error en la transmissió dels registres: {{errorMessage}}"
|
"whileStreamingLogs": "Error en la transmissió dels registres: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"websocket": {
|
||||||
|
"label": "Missatges",
|
||||||
|
"pause": "Pausa",
|
||||||
|
"resume": "Reprèn",
|
||||||
|
"clear": "Neteja",
|
||||||
|
"filter": {
|
||||||
|
"all": "Tots els temes",
|
||||||
|
"topics": "Temes",
|
||||||
|
"events": "Esdeveniment",
|
||||||
|
"reviews": "Revisions",
|
||||||
|
"classification": "Classificació",
|
||||||
|
"face_recognition": "Reconeixement facial",
|
||||||
|
"lpr": "LPR",
|
||||||
|
"camera_activity": "Activitat de la càmera",
|
||||||
|
"system": "Sistema",
|
||||||
|
"camera": "Càmara",
|
||||||
|
"all_cameras": "Totes les càmeres",
|
||||||
|
"cameras_count_one": "{{count}} càmera",
|
||||||
|
"cameras_count_other": "{{count}} Càmeres"
|
||||||
|
},
|
||||||
|
"empty": "Encara no s'ha capturat cap missatge",
|
||||||
|
"count": "{{count}} missatges",
|
||||||
|
"expanded": {
|
||||||
|
"payload": "Payload"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
@ -80,8 +107,10 @@
|
|||||||
"intelGpuWarning": {
|
"intelGpuWarning": {
|
||||||
"title": "Avís d'estadístiques de la GPU d'Intel",
|
"title": "Avís d'estadístiques de la GPU d'Intel",
|
||||||
"message": "Estadístiques de GPU no disponibles",
|
"message": "Estadístiques de GPU no disponibles",
|
||||||
"description": "Aquest és un error conegut en les eines d'informació de les estadístiques de GPU d'Intel (intel.gpu.top) on es trencarà i retornarà repetidament un ús de GPU del 0% fins i tot en els casos en què l'acceleració del maquinari i la detecció d'objectes s'executen correctament a la (i)GPU. Això no és un error de fragata. Podeu reiniciar l'amfitrió per a corregir temporalment el problema i confirmar que la GPU funciona correctament. Això no afecta el rendiment."
|
"description": "Aquest és un error conegut en les eines d'informació de les estadístiques de GPU d'Intel (intel.gpu.top) on es trencarà i retornarà repetidament un ús de GPU del 0% fins i tot en els casos en què l'acceleració del maquinari i la detecció d'objectes s'executen correctament a la (i)GPU. Això no és un error de Frigate. Podeu reiniciar l'amfitrió per a corregir temporalment el problema i confirmar que la GPU funciona correctament. Això no afecta el rendiment."
|
||||||
}
|
},
|
||||||
|
"gpuTemperature": "Temperatura de la GPU",
|
||||||
|
"npuTemperature": "Temperatura NPU"
|
||||||
},
|
},
|
||||||
"otherProcesses": {
|
"otherProcesses": {
|
||||||
"title": "Altres processos",
|
"title": "Altres processos",
|
||||||
@ -165,6 +194,17 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"unableToProbeCamera": "No s'ha pogut sondejar la càmera: {{errorMessage}}"
|
"unableToProbeCamera": "No s'ha pogut sondejar la càmera: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"connectionQuality": {
|
||||||
|
"title": "Qualitat de la connexió",
|
||||||
|
"excellent": "Excel·lent",
|
||||||
|
"fair": "Fira",
|
||||||
|
"poor": "Pobre",
|
||||||
|
"unusable": "No utilitzable",
|
||||||
|
"fps": "FPS",
|
||||||
|
"expectedFps": "FPS esperat",
|
||||||
|
"reconnectsLastHour": "Reconnecta (última hora)",
|
||||||
|
"stallsLastHour": "Parades (última hora)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastRefreshed": "Darrera actualització: ",
|
"lastRefreshed": "Darrera actualització: ",
|
||||||
@ -176,7 +216,8 @@
|
|||||||
"detectHighCpuUsage": "{{camera}} te un ús elevat de CPU per la detecció ({{detectAvg}}%)",
|
"detectHighCpuUsage": "{{camera}} te un ús elevat de CPU per la detecció ({{detectAvg}}%)",
|
||||||
"detectIsVerySlow": "{{detect}} és molt lent ({{speed}} ms)",
|
"detectIsVerySlow": "{{detect}} és molt lent ({{speed}} ms)",
|
||||||
"detectIsSlow": "{{detect}} és lent ({{speed}} ms)",
|
"detectIsSlow": "{{detect}} és lent ({{speed}} ms)",
|
||||||
"shmTooLow": "/dev/shm directori ({{total}} MB) hauria de ser incrementat com a mínim {{min}} MB."
|
"shmTooLow": "/dev/shm directori ({{total}} MB) hauria de ser incrementat com a mínim {{min}} MB.",
|
||||||
|
"debugReplayActive": "La sessió de repetició de depuració està activa"
|
||||||
},
|
},
|
||||||
"enrichments": {
|
"enrichments": {
|
||||||
"title": "Enriquiments",
|
"title": "Enriquiments",
|
||||||
|
|||||||
@ -133,7 +133,7 @@
|
|||||||
},
|
},
|
||||||
"unit": {
|
"unit": {
|
||||||
"speed": {
|
"speed": {
|
||||||
"kph": "Km/h",
|
"kph": "km/h",
|
||||||
"mph": "míle/h"
|
"mph": "míle/h"
|
||||||
},
|
},
|
||||||
"length": {
|
"length": {
|
||||||
@ -177,7 +177,7 @@
|
|||||||
"fi": "Suomi (Finština)",
|
"fi": "Suomi (Finština)",
|
||||||
"sk": "Slovenčina (Slovenština)",
|
"sk": "Slovenčina (Slovenština)",
|
||||||
"withSystem": {
|
"withSystem": {
|
||||||
"label": "Použít systémové nastavení pro jazyk"
|
"label": "Použít systémové nastavení jazyka"
|
||||||
},
|
},
|
||||||
"zhCN": "简体中文 (Zjednodušená čínština)",
|
"zhCN": "简体中文 (Zjednodušená čínština)",
|
||||||
"es": "Español (Španělština)",
|
"es": "Español (Španělština)",
|
||||||
@ -205,14 +205,15 @@
|
|||||||
"pl": "Polski (Polština)",
|
"pl": "Polski (Polština)",
|
||||||
"th": "ไทย (Thaiština)",
|
"th": "ไทย (Thaiština)",
|
||||||
"ca": "Català (Katalánština)",
|
"ca": "Català (Katalánština)",
|
||||||
"sl": "Slovinština (Slovinsko)",
|
"sl": "Slovinština (Slovinština)",
|
||||||
"ptBR": "Português brasileiro (Brazilian Portuguese)",
|
"ptBR": "Português brasileiro (Brazilská Portugalština)",
|
||||||
"sr": "Српски (Serbian)",
|
"sr": "Српски (Srbština)",
|
||||||
"lt": "Lietuvių (Lithuanian)",
|
"lt": "Lietuvių (Litevština)",
|
||||||
"bg": "Български (Bulgarian)",
|
"bg": "Български (Bulharština)",
|
||||||
"gl": "Galego (Galician)",
|
"gl": "Galego (Galicijština)",
|
||||||
"id": "Bahasa Indonesia (Indonesian)",
|
"id": "Bahasa Indonesia (Indonéština)",
|
||||||
"ur": "اردو (Urdu)"
|
"ur": "اردو (Urdština)",
|
||||||
|
"hr": "Hrvatski (Chorvatština)"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"highcontrast": "Vysoký kontrast",
|
"highcontrast": "Vysoký kontrast",
|
||||||
|
|||||||
@ -123,7 +123,18 @@
|
|||||||
"on": "AN",
|
"on": "AN",
|
||||||
"suspended": "Pausierte",
|
"suspended": "Pausierte",
|
||||||
"unsuspended": "fortsetzen",
|
"unsuspended": "fortsetzen",
|
||||||
"continue": "Weiter"
|
"continue": "Weiter",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"applying": "Wird angewendet…",
|
||||||
|
"undo": "Rückgängig",
|
||||||
|
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||||
|
"modified": "Verändert",
|
||||||
|
"overridden": "Überschrieben",
|
||||||
|
"resetToGlobal": "Auf Global zurückgesetzen",
|
||||||
|
"resetToDefault": "Auf Werkseinstellungen zurücksetzten",
|
||||||
|
"saveAll": "Alle speichern",
|
||||||
|
"savingAll": "Alle werden gespeichert…",
|
||||||
|
"undoAll": "Alle rückgängig"
|
||||||
},
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
|
|||||||
@ -74,7 +74,11 @@
|
|||||||
"saveExport": "Export speichern",
|
"saveExport": "Export speichern",
|
||||||
"previewExport": "Exportvorschau"
|
"previewExport": "Exportvorschau"
|
||||||
},
|
},
|
||||||
"export": "Exportieren"
|
"export": "Exportieren",
|
||||||
|
"case": {
|
||||||
|
"label": "Fall",
|
||||||
|
"placeholder": "Einen Fall auswählen"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"streaming": {
|
"streaming": {
|
||||||
"restreaming": {
|
"restreaming": {
|
||||||
|
|||||||
81
web/public/locales/de/config/cameras.json
Normal file
81
web/public/locales/de/config/cameras.json
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"label": "KameraEinstellungen",
|
||||||
|
"name": {
|
||||||
|
"label": "Name der Kamera",
|
||||||
|
"description": "Kameraname ist erforderlich"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"label": "Aktiviert",
|
||||||
|
"description": "Aktiviert"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"label": "Audioereignisse",
|
||||||
|
"description": "Einstellungen für audiobasierte Ereigniserkennung für diese Kamera.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Aktivieren der Audioerkennung",
|
||||||
|
"description": "Aktivieren / Deaktivieren der audiobasierten Ereigniserkennung für diese Kamera."
|
||||||
|
},
|
||||||
|
"min_volume": {
|
||||||
|
"label": "Mindestlautstärke",
|
||||||
|
"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).",
|
||||||
|
"label": "Hörtypen"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Audiofilter",
|
||||||
|
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
|
||||||
|
},
|
||||||
|
"max_not_heard": {
|
||||||
|
"label": "Zeitüberschreitung beendet",
|
||||||
|
"description": "Anzahl der Sekunden ohne den konfigurierten Audiotyp, bevor das Audioereignis beendet wird."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Ursprünglicher Audiozustand",
|
||||||
|
"description": "Gibt an, ob die Audioerkennung ursprünglich in der statischen Konfigurationsdatei aktiviert war."
|
||||||
|
},
|
||||||
|
"num_threads": {
|
||||||
|
"label": "Erkennungsthreads",
|
||||||
|
"description": "Anzahl der Threads, die für die Audioerkennungsverarbeitung verwendet werden sollen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"friendly_name": {
|
||||||
|
"label": "Anzeigename",
|
||||||
|
"description": "Kamera-Anzeigename in der Frigate-Benutzeroberfläche"
|
||||||
|
},
|
||||||
|
"audio_transcription": {
|
||||||
|
"label": "Audio-Transkription",
|
||||||
|
"description": "Einstellungen für Live- und Sprach-Audio-Transkription, die für Veranstaltungen und Live-Untertitel verwendet werden.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Transkription aktivieren",
|
||||||
|
"description": "Aktivieren oder deaktivieren Sie die manuell ausgelöste Transkription von Audioereignissen."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Ursprünglicher Transkriptionszustand"
|
||||||
|
},
|
||||||
|
"live_enabled": {
|
||||||
|
"label": "Live-Transkription",
|
||||||
|
"description": "Aktivieren Sie die Live-Transkription für Audio, sobald es empfangen wird."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birdseye": {
|
||||||
|
"label": "Birdseye",
|
||||||
|
"description": "Einstellungen für die Birdseye-Kompositansicht, die mehrere Kamerafeeds zu einem einzigen Layout zusammenfasst.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Birdseye aktivieren",
|
||||||
|
"description": "Aktivieren oder deaktivieren der Birdseye-Funktion."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Verfolgungsmodus",
|
||||||
|
"description": "Modus zum Einbeziehen von Kameras in Birdseye: „Objekte“, „Bewegung“ oder „kontinuierlich“."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"label": "Position",
|
||||||
|
"description": "Numerische Position, die die Reihenfolge der Kamera im Birdseye-Layout steuert."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"label": "Objekterkennung"
|
||||||
|
}
|
||||||
|
}
|
||||||
138
web/public/locales/de/config/global.json
Normal file
138
web/public/locales/de/config/global.json
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"version": {
|
||||||
|
"label": "Aktuelle Version der Konfiguration",
|
||||||
|
"description": "Die Version Numerisch oder als Zeichenketten der aktiven Konfiguration, um Migrationen oder Formatänderungen zu erkennen."
|
||||||
|
},
|
||||||
|
"safe_mode": {
|
||||||
|
"label": "abgesicherter Modus",
|
||||||
|
"description": "Wenn aktiviert, starte Frigate im abgesicherten Modus mit reduzierten Features für die Fehlersuche."
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"label": "Audioereignisse",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Aktivieren der Audioerkennung"
|
||||||
|
},
|
||||||
|
"min_volume": {
|
||||||
|
"label": "Mindestlautstärke",
|
||||||
|
"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).",
|
||||||
|
"label": "Hörtypen"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Audiofilter",
|
||||||
|
"description": "Filtereinstellungen pro Audiotyp, wie z. B. Konfidenzschwellenwerte, die zur Reduzierung von Fehlalarmen verwendet werden."
|
||||||
|
},
|
||||||
|
"max_not_heard": {
|
||||||
|
"label": "Zeitüberschreitung beendet",
|
||||||
|
"description": "Anzahl der Sekunden ohne den konfigurierten Audiotyp, bevor das Audioereignis beendet wird."
|
||||||
|
},
|
||||||
|
"enabled_in_config": {
|
||||||
|
"label": "Ursprünglicher Audiozustand",
|
||||||
|
"description": "Gibt an, ob die Audioerkennung ursprünglich in der statischen Konfigurationsdatei aktiviert war."
|
||||||
|
},
|
||||||
|
"num_threads": {
|
||||||
|
"label": "Erkennungsthreads",
|
||||||
|
"description": "Anzahl der Threads, die für die Audioerkennungsverarbeitung verwendet werden sollen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"environment_vars": {
|
||||||
|
"label": "Umgebungsvariablen",
|
||||||
|
"description": "Schlüssel-/Wertpaare für Umgebungsvariablen des Frigate-Prozesses in Home Assistant OS. Nicht-HAOS Benutzer müssen anstatt dessen Docker Umgebungsvariablen nutzen."
|
||||||
|
},
|
||||||
|
"logger": {
|
||||||
|
"label": "Protokollierung",
|
||||||
|
"description": "Steuert die Standard-Protokollierungsausführlichkeit und Überschreibungen der Protokollierungsstufe pro Komponente.",
|
||||||
|
"default": {
|
||||||
|
"label": "Loglevel",
|
||||||
|
"description": "Standardmäßiges globales Log Level (Debug, Info, Warnung, Fehler)."
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"label": "Prozessspezifisches Log Level",
|
||||||
|
"description": "Überschreiben der Protokollierungsstufe pro Komponente, um die Ausführlichkeit für bestimmte Module zu erhöhen oder zu verringern."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"label": "Authentifizierung",
|
||||||
|
"description": "Einstellungen für die Authentifizierung und Sitzungen, einschließlich Optionen für Cookies und Limits.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Authentifizierung aktivieren",
|
||||||
|
"description": "Aktivierung native Authentifizierung für Frigate UI."
|
||||||
|
},
|
||||||
|
"reset_admin_password": {
|
||||||
|
"label": "Zurücksetzen vom Admin Passwort",
|
||||||
|
"description": "Wenn wahr, wird das Passwort beim nächsten start zurückgesetzt und das neue Passwort steht in den Logs."
|
||||||
|
},
|
||||||
|
"cookie_name": {
|
||||||
|
"label": "JWT cookie Name",
|
||||||
|
"description": "Name des Cookies, das zum Speichern des JWT-Tokens für die native Authentifizierung verwendet wird."
|
||||||
|
},
|
||||||
|
"cookie_secure": {
|
||||||
|
"label": "Sicheres Cookie-Flag",
|
||||||
|
"description": "Setzen Sie das Sicherheitsflag im Authentifizierungs-Cookie; sollte bei Verwendung von TLS auf „true“ gesetzt sein."
|
||||||
|
},
|
||||||
|
"session_length": {
|
||||||
|
"label": "Sitzungssdauer",
|
||||||
|
"description": "Sitzungsdauer in Sekunden für JWT-basierte Sitzungen."
|
||||||
|
},
|
||||||
|
"refresh_time": {
|
||||||
|
"label": "Sitzung aktualisieren",
|
||||||
|
"description": "Wenn eine Sitzung innerhalb dieser Sekunden abläuft, aktualisieren Sie sie wieder auf ihre volle Länge."
|
||||||
|
},
|
||||||
|
"failed_login_rate_limit": {
|
||||||
|
"label": "Fehlgeschlagene Anmeldeversuche",
|
||||||
|
"description": "Begrenzungsregeln für fehlgeschlagene Anmeldeversuche zur Reduzierung von Brute-Force-Angriffen."
|
||||||
|
},
|
||||||
|
"trusted_proxies": {
|
||||||
|
"label": "Vertrauenswürdige Proxys",
|
||||||
|
"description": "Liste vertrauenswürdiger Proxy-IPs, die bei der Ermittlung der Client-IP für die Ratenbegrenzung verwendet werden."
|
||||||
|
},
|
||||||
|
"hash_iterations": {
|
||||||
|
"label": "Hash-Iterationen",
|
||||||
|
"description": "Anzahl der PBKDF2-SHA256-Iterationen, die beim Hashing von Benutzerkennwörtern verwendet werden sollen."
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"label": "Rollen zuweisen",
|
||||||
|
"description": "Ordnen Sie Rollen zu Kameralisten zu. Eine leere Liste gewährt der Rolle Zugriff auf alle Kameras."
|
||||||
|
},
|
||||||
|
"admin_first_time_login": {
|
||||||
|
"label": "Erstmalige Admin-Markierung",
|
||||||
|
"description": "Wenn dies zutrifft, zeigt die Benutzeroberfläche möglicherweise einen Hilfe-Link auf der Anmeldeseite an, der Benutzer darüber informiert, wie sie sich nach einer Zurücksetzung des Administratorpassworts anmelden können. "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio_transcription": {
|
||||||
|
"label": "Audio-Transkription",
|
||||||
|
"description": "Einstellungen für Live- und Sprach-Audio-Transkription, die für Veranstaltungen und Live-Untertitel verwendet werden.",
|
||||||
|
"live_enabled": {
|
||||||
|
"label": "Live-Transkription",
|
||||||
|
"description": "Aktivieren Sie die Live-Transkription für Audio, sobald es empfangen wird."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"birdseye": {
|
||||||
|
"label": "Birdseye",
|
||||||
|
"description": "Einstellungen für die Birdseye-Kompositansicht, die mehrere Kamerafeeds zu einem einzigen Layout zusammenfasst.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Birdseye aktivieren",
|
||||||
|
"description": "Aktivieren oder deaktivieren der Birdseye-Funktion."
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"label": "Verfolgungsmodus",
|
||||||
|
"description": "Modus zum Einbeziehen von Kameras in Birdseye: „Objekte“, „Bewegung“ oder „kontinuierlich“."
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"label": "Position",
|
||||||
|
"description": "Numerische Position, die die Reihenfolge der Kamera im Birdseye-Layout steuert."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"label": "Datenbank",
|
||||||
|
"description": "Einstellungen für die SQLite-Datenbank, die von Frigate zum Speichern von verfolgten Objekten und Aufzeichnungsmetadaten verwendet wird.",
|
||||||
|
"path": {
|
||||||
|
"label": "Pfad zur Datenbank"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"label": "Objekterkennung"
|
||||||
|
}
|
||||||
|
}
|
||||||
73
web/public/locales/de/config/groups.json
Normal file
73
web/public/locales/de/config/groups.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"global": {
|
||||||
|
"detection": "Globale Erkennung",
|
||||||
|
"sensitivity": "Globale Empfindlichkeit"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"detection": "Erkennung",
|
||||||
|
"sensitivity": "Empfindlichkeit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp_style": {
|
||||||
|
"global": {
|
||||||
|
"appearance": "Globale Darstellung"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"appearance": "Erscheinungsbild"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"global": {
|
||||||
|
"sensitivity": "Globale Empfindlichkeit",
|
||||||
|
"algorithm": "Globaler Algorithmus"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"sensitivity": "Genauhigkeit",
|
||||||
|
"algorithm": "Algorithmus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"global": {
|
||||||
|
"display": "Globale Anzeige"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"display": "Anzeige"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"global": {
|
||||||
|
"resolution": "Globale Auflösung",
|
||||||
|
"tracking": "Globale Verfolgung"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"resolution": "Auflösung",
|
||||||
|
"tracking": "Verfolgung"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"global": {
|
||||||
|
"tracking": "Globale Verfolgung",
|
||||||
|
"filtering": "Globaler Filter"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"tracking": "Verfolgung",
|
||||||
|
"filtering": "Filtern"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"record": {
|
||||||
|
"global": {
|
||||||
|
"retention": "Globale Bindung",
|
||||||
|
"events": "Globale Ereignisse"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"retention": "Bindung",
|
||||||
|
"events": "Events"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"cameras": {
|
||||||
|
"cameraFfmpeg": "Kameraspezifische FFmpeg-Argumente"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
web/public/locales/de/config/validation.json
Normal file
32
web/public/locales/de/config/validation.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"maximum": "Darf nicht größer sein als {{limit}}",
|
||||||
|
"minimum": "Darf nicht kleiner sein als {{limit}}",
|
||||||
|
"exclusiveMinimum": "Muss größer sein als {{limit}}",
|
||||||
|
"minLength": "Muss mindestens {{limit}} Zeichen lang sein",
|
||||||
|
"maxLength": "Muss maximal {{limit}} Zeichen lang sein",
|
||||||
|
"minItems": "Muss mindestens {{limit}} mal vorkommen",
|
||||||
|
"exclusiveMaximum": "Muss kleiner sein als {{limit}}",
|
||||||
|
"maxItems": "Muss maximal {{limit}} mal vorkommen",
|
||||||
|
"pattern": "Ungültiges Format",
|
||||||
|
"required": "Pflichtfeld",
|
||||||
|
"type": "Ungültiger Wertetyp",
|
||||||
|
"enum": "Muss einer der erlaubten Werte sein",
|
||||||
|
"const": "Wert stimmt nicht mit erwarteter Konstante überein",
|
||||||
|
"uniqueItems": "Alle Einträge müssen eindeutig sein",
|
||||||
|
"format": "Ungültiges Format",
|
||||||
|
"additionalProperties": "Unbekannte Eigenschaft ist nicht erlaubt",
|
||||||
|
"oneOf": "Muss exakt mit einem der erlaubten Schemas übereinstimmen",
|
||||||
|
"anyOf": "Muss mindestens mit einem der erlaubten Schemas übereinstimmen",
|
||||||
|
"proxy": {
|
||||||
|
"header_map": {
|
||||||
|
"roleHeaderRequired": "Rollen-Header muss angegeben werden, wenn Rollen-Zuordnungen konfiguriert sind."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": {
|
||||||
|
"rolesUnique": "Jede Rolle kann nur einem input stream zugeteilt werden.",
|
||||||
|
"detectRequired": "Es muss mindestens ein input stream die Rolle 'erkennen' tragen.",
|
||||||
|
"hwaccelDetectOnly": "Nur der input-stream mit der Rolle 'erkennen' kann Hardwarebeschleunigungs Argumente definieren."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,13 +11,27 @@
|
|||||||
"noExports": "Keine Exporte gefunden",
|
"noExports": "Keine Exporte gefunden",
|
||||||
"toast": {
|
"toast": {
|
||||||
"error": {
|
"error": {
|
||||||
"renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}"
|
"renameExportFailed": "Umbenennen des Exports fehlgeschlagen: {{errorMessage}}",
|
||||||
|
"assignCaseFailed": "Aktualisierung der Fallzuweisung fehlgeschlagen: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"shareExport": "Export teilen",
|
"shareExport": "Export teilen",
|
||||||
"downloadVideo": "Video herunterladen",
|
"downloadVideo": "Video herunterladen",
|
||||||
"editName": "Name ändern",
|
"editName": "Name ändern",
|
||||||
"deleteExport": "Export löschen"
|
"deleteExport": "Export löschen",
|
||||||
|
"assignToCase": "Hinzufügen zum Fall"
|
||||||
|
},
|
||||||
|
"headings": {
|
||||||
|
"cases": "Fälle",
|
||||||
|
"uncategorizedExports": "Unkategorisierte Exporte"
|
||||||
|
},
|
||||||
|
"caseDialog": {
|
||||||
|
"title": "Zum Fall hinzufügen",
|
||||||
|
"description": "Wählen Sie einen bestehenden Fall aus oder erstellen Sie einen neuen.",
|
||||||
|
"selectLabel": "Fall",
|
||||||
|
"newCaseOption": "Neuen Fall erstellen",
|
||||||
|
"nameLabel": "Fallname",
|
||||||
|
"descriptionLabel": "Beschreibung"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,14 +5,17 @@
|
|||||||
"camera": "Kameraeinstellungen - Frigate",
|
"camera": "Kameraeinstellungen - Frigate",
|
||||||
"masksAndZones": "Masken- und Zoneneditor – Frigate",
|
"masksAndZones": "Masken- und Zoneneditor – Frigate",
|
||||||
"object": "Debug - Frigate",
|
"object": "Debug - Frigate",
|
||||||
"general": "UI-Einstellungen - Frigate",
|
"general": "Profileinstellungen - Frigate",
|
||||||
"frigatePlus": "Frigate+ Einstellungen – Frigate",
|
"frigatePlus": "Frigate+ Einstellungen – Frigate",
|
||||||
"classification": "Klassifizierungseinstellungen – Frigate",
|
"classification": "Klassifizierungseinstellungen – Frigate",
|
||||||
"motionTuner": "Bewegungserkennungs-Optimierer – Frigate",
|
"motionTuner": "Bewegungserkennungs-Optimierer – Frigate",
|
||||||
"notifications": "Benachrichtigungseinstellungen",
|
"notifications": "Benachrichtigungseinstellungen",
|
||||||
"enrichments": "Erweiterte Statistiken - Frigate",
|
"enrichments": "Erweiterte Statistiken - Frigate",
|
||||||
"cameraManagement": "Kameras verwalten - Frigate",
|
"cameraManagement": "Kameras verwalten - Frigate",
|
||||||
"cameraReview": "Kameraeinstellungen prüfen - Frigate"
|
"cameraReview": "Kameraeinstellungen prüfen - Frigate",
|
||||||
|
"globalConfig": "Globale Konfiguration - Frigate",
|
||||||
|
"cameraConfig": "Kamera Konfiguration - Frigate",
|
||||||
|
"maintenance": "Wartung - Frigate"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"ui": "Benutzeroberfläche",
|
"ui": "Benutzeroberfläche",
|
||||||
@ -28,7 +31,31 @@
|
|||||||
"triggers": "Auslöser",
|
"triggers": "Auslöser",
|
||||||
"roles": "Rollen",
|
"roles": "Rollen",
|
||||||
"cameraManagement": "Verwaltung",
|
"cameraManagement": "Verwaltung",
|
||||||
"cameraReview": "Überprüfung"
|
"cameraReview": "Überprüfung",
|
||||||
|
"system": "System",
|
||||||
|
"general": "allgemein",
|
||||||
|
"globalConfig": "Globale Konfiguration",
|
||||||
|
"integrations": "Integrationen",
|
||||||
|
"profileSettings": "Profil Einstellung",
|
||||||
|
"globalDetect": "Objekterkennung",
|
||||||
|
"globalRecording": "Aufnahme",
|
||||||
|
"globalSnapshots": "Schnappschüsse",
|
||||||
|
"globalFfmpeg": "FFmpeg",
|
||||||
|
"globalMotion": "Bewegungserkennung",
|
||||||
|
"globalObjects": "Objekte",
|
||||||
|
"globalReview": "Überprüfung",
|
||||||
|
"globalAudioEvents": "Audio Events",
|
||||||
|
"globalLivePlayback": "Live Wiedergabe",
|
||||||
|
"globalTimestampStyle": "Zeitstempelformat",
|
||||||
|
"systemDatabase": "Datenbank",
|
||||||
|
"systemTls": "TLS",
|
||||||
|
"systemAuthentication": "Authentifizierung",
|
||||||
|
"systemNetworking": "Netzwerk",
|
||||||
|
"systemProxy": "Proxy",
|
||||||
|
"systemUi": "UI",
|
||||||
|
"systemLogging": "Log",
|
||||||
|
"systemEnvironmentVariables": "Umgebungsvariablen",
|
||||||
|
"systemTelemetry": "Telemetrie"
|
||||||
},
|
},
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"unsavedChanges": {
|
"unsavedChanges": {
|
||||||
@ -41,7 +68,7 @@
|
|||||||
"noCamera": "Keine Kamera"
|
"noCamera": "Keine Kamera"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"title": "Einstellungen der Benutzeroberfläche",
|
"title": "Profileinstellungen",
|
||||||
"liveDashboard": {
|
"liveDashboard": {
|
||||||
"title": "Live Übersicht",
|
"title": "Live Übersicht",
|
||||||
"playAlertVideos": {
|
"playAlertVideos": {
|
||||||
@ -408,7 +435,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"motionMaskLabel": "Bewegungsmaske {{number}}",
|
"motionMaskLabel": "Bewegungsmaske {{number}}",
|
||||||
"objectMaskLabel": "Objektmaske {{number}} ({{label}})"
|
"objectMaskLabel": "Objektmaske {{number}}"
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"objectShapeFilterDrawing": {
|
"objectShapeFilterDrawing": {
|
||||||
|
|||||||
@ -67,7 +67,8 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"frigate": "Frigate Protokolle – Frigate",
|
"frigate": "Frigate Protokolle – Frigate",
|
||||||
"go2rtc": "Go2RTC Protokolle - Frigate",
|
"go2rtc": "Go2RTC Protokolle - Frigate",
|
||||||
"nginx": "Nginx Protokolle - Frigate"
|
"nginx": "Nginx Protokolle - Frigate",
|
||||||
|
"websocket": "Nachrichten Logs- Frigate"
|
||||||
},
|
},
|
||||||
"enrichments": "Erweiterte Statistiken - Frigate"
|
"enrichments": "Erweiterte Statistiken - Frigate"
|
||||||
},
|
},
|
||||||
@ -93,7 +94,32 @@
|
|||||||
"whileStreamingLogs": "Beim Übertragen der Protokolle ist ein Fehler aufgetreten: {{errorMessage}}"
|
"whileStreamingLogs": "Beim Übertragen der Protokolle ist ein Fehler aufgetreten: {{errorMessage}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tips": "Protokolle werden in Echtzeit vom Server übertragen"
|
"tips": "Protokolle werden in Echtzeit vom Server übertragen",
|
||||||
|
"websocket": {
|
||||||
|
"label": "Nachrichten",
|
||||||
|
"pause": "Pause",
|
||||||
|
"clear": "Säubern",
|
||||||
|
"filter": {
|
||||||
|
"all": "alle Themen",
|
||||||
|
"topics": "Themen",
|
||||||
|
"events": "Event",
|
||||||
|
"reviews": "Bewertungen",
|
||||||
|
"classification": "Klassifizierung",
|
||||||
|
"face_recognition": "Gesichtserkennung",
|
||||||
|
"lpr": "LPR",
|
||||||
|
"camera_activity": "Kameraaktivität",
|
||||||
|
"system": "System",
|
||||||
|
"camera": "Kamera",
|
||||||
|
"all_cameras": "Alle Kameras",
|
||||||
|
"cameras_count_one": "{{count}} Kamera",
|
||||||
|
"cameras_count_other": "{{count}} Kameras"
|
||||||
|
},
|
||||||
|
"empty": "Noch keine Nachrichten erfasst",
|
||||||
|
"count": "{{count}} Nachrichten",
|
||||||
|
"expanded": {
|
||||||
|
"payload": "Nutzlast"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"metrics": "Systemmetriken",
|
"metrics": "Systemmetriken",
|
||||||
"storage": {
|
"storage": {
|
||||||
|
|||||||
@ -80,6 +80,9 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"empty": "No previews available",
|
"empty": "No previews available",
|
||||||
"noPreview": "Preview unavailable",
|
"noPreview": "Preview unavailable",
|
||||||
"seekAria": "Seek {{camera}} player to {{time}}"
|
"seekAria": "Seek {{camera}} player to {{time}}",
|
||||||
|
"filter": "Filter",
|
||||||
|
"filterDesc": "Select areas to only show clips with motion in those regions.",
|
||||||
|
"filterClear": "Clear"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -422,6 +422,18 @@
|
|||||||
"cameraManagement": {
|
"cameraManagement": {
|
||||||
"title": "Manage Cameras",
|
"title": "Manage Cameras",
|
||||||
"addCamera": "Add New Camera",
|
"addCamera": "Add New Camera",
|
||||||
|
"deleteCamera": "Delete Camera",
|
||||||
|
"deleteCameraDialog": {
|
||||||
|
"title": "Delete Camera",
|
||||||
|
"description": "Deleting a camera will permanently remove all recordings, tracked objects, and configuration for that camera. Any go2rtc streams associated with this camera may still need to be manually removed.",
|
||||||
|
"selectPlaceholder": "Choose camera...",
|
||||||
|
"confirmTitle": "Are you sure?",
|
||||||
|
"confirmWarning": "Deleting <strong>{{cameraName}}</strong> cannot be undone.",
|
||||||
|
"deleteExports": "Also delete exports for this camera",
|
||||||
|
"confirmButton": "Delete Permanently",
|
||||||
|
"success": "Camera {{cameraName}} deleted successfully",
|
||||||
|
"error": "Failed to delete camera {{cameraName}}"
|
||||||
|
},
|
||||||
"editCamera": "Edit Camera:",
|
"editCamera": "Edit Camera:",
|
||||||
"selectCamera": "Select a Camera",
|
"selectCamera": "Select a Camera",
|
||||||
"backToSettings": "Back to Camera Settings",
|
"backToSettings": "Back to Camera Settings",
|
||||||
|
|||||||
@ -139,7 +139,11 @@
|
|||||||
},
|
},
|
||||||
"shm": {
|
"shm": {
|
||||||
"title": "SHM (shared memory) allocation",
|
"title": "SHM (shared memory) allocation",
|
||||||
"warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB."
|
"warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB.",
|
||||||
|
"frameLifetime": {
|
||||||
|
"title": "Frame lifetime",
|
||||||
|
"description": "Each camera has {{frames}} frame slots in shared memory. At the fastest camera's frame rate, each frame is available for approximately {{lifetime}}s before being overwritten."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"cameraStorage": {
|
"cameraStorage": {
|
||||||
"title": "Camera Storage",
|
"title": "Camera Storage",
|
||||||
|
|||||||
30
web/public/locales/es/config/cameras.json
Normal file
30
web/public/locales/es/config/cameras.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": {
|
||||||
|
"label": "Nombre de cámara",
|
||||||
|
"description": "El nombre de la cámara es necesario"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilitado",
|
||||||
|
"description": "Habilitado"
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"label": "Eventos de audio",
|
||||||
|
"description": "Configuración para la detección de eventos basada en audio para esta cámara.",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilitar la detección de audio",
|
||||||
|
"description": "Activar o deshabilitar la detección de eventos de audio para esta cámara."
|
||||||
|
},
|
||||||
|
"max_not_heard": {
|
||||||
|
"label": "Finalizar el tiempo de espera",
|
||||||
|
"description": "Cantidad de segundos sin el tipo de audio configurado antes de que finalice el evento de audio."
|
||||||
|
},
|
||||||
|
"min_volume": {
|
||||||
|
"label": "Volumen mínimo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"friendly_name": {
|
||||||
|
"label": "Nombre descriptivo",
|
||||||
|
"description": "Nombre descriptivo de la cámara utilizado en la interfaz de usuario de Frigate"
|
||||||
|
},
|
||||||
|
"label": "Configuración de Cámara"
|
||||||
|
}
|
||||||
43
web/public/locales/es/config/global.json
Normal file
43
web/public/locales/es/config/global.json
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"version": {
|
||||||
|
"label": "Versión de configuración actual",
|
||||||
|
"description": "Versión numérica o de cadena de la configuración activa para ayudar a detectar migraciones o cambios de formato."
|
||||||
|
},
|
||||||
|
"safe_mode": {
|
||||||
|
"label": "Modo seguro",
|
||||||
|
"description": "Cuando está habilitado, inicia Frigate en modo seguro con funciones reducidas para la solución de problemas."
|
||||||
|
},
|
||||||
|
"environment_vars": {
|
||||||
|
"label": "Variables de entorno",
|
||||||
|
"description": "Pares clave/valor de variables de entorno para establecer para el proceso de Frigate en el sistema operativo Home Assistant. Los usuarios que no son de HAOS deben usar la configuración de variables de entorno de Docker."
|
||||||
|
},
|
||||||
|
"logger": {
|
||||||
|
"label": "Registro",
|
||||||
|
"description": "Controla la verbosidad de registro predeterminada y la sobre-escritura de nivel de registro por componente.",
|
||||||
|
"default": {
|
||||||
|
"label": "Nivel de registro",
|
||||||
|
"description": "Nivel de detalle global predeterminada del registro (depuración, información, advertencia, error)."
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"label": "Nivel de registro por proceso",
|
||||||
|
"description": "Sobre-escribir el nivel de registro por componente para aumentar o disminuir el nivel de detalle de módulos específicos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"audio": {
|
||||||
|
"label": "Eventos de audio",
|
||||||
|
"enabled": {
|
||||||
|
"label": "Habilitar la detección de audio"
|
||||||
|
},
|
||||||
|
"max_not_heard": {
|
||||||
|
"label": "Finalizar el tiempo de espera",
|
||||||
|
"description": "Cantidad de segundos sin el tipo de audio configurado antes de que finalice el evento de audio."
|
||||||
|
},
|
||||||
|
"min_volume": {
|
||||||
|
"label": "Volumen mínimo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"label": "Autenticación",
|
||||||
|
"description": "Configuración relacionada con la autenticación y la sesión, incluidas las opciones de cookies y límite de peticiones."
|
||||||
|
}
|
||||||
|
}
|
||||||
44
web/public/locales/es/config/groups.json
Normal file
44
web/public/locales/es/config/groups.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"global": {
|
||||||
|
"detection": "Detección Global",
|
||||||
|
"sensitivity": "Sensibilidad Global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"detection": "Detección",
|
||||||
|
"sensitivity": "Sensibilidad"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp_style": {
|
||||||
|
"global": {
|
||||||
|
"appearance": "Apariencia Global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"appearance": "Apariencia"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"motion": {
|
||||||
|
"global": {
|
||||||
|
"sensitivity": "Sensibilidad Global",
|
||||||
|
"algorithm": "Algoritmo Global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"sensitivity": "Sensibilidad",
|
||||||
|
"algorithm": "Algoritmo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"global": {
|
||||||
|
"display": "Pantalla Global"
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
"display": "Pantalla"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"detect": {
|
||||||
|
"global": {
|
||||||
|
"resolution": "Resolución Global",
|
||||||
|
"tracking": "Seguimiento Global"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user